volute 0.21.0 → 0.23.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 (58) hide show
  1. package/dist/api.d.ts +4294 -0
  2. package/dist/chunk-G5KRTU2F.js +76 -0
  3. package/dist/chunk-ISWZ6QUK.js +2691 -0
  4. package/dist/{chunk-J5A3DF2U.js → chunk-JG4CCJOA.js} +1 -1
  5. package/dist/{chunk-IPJXU366.js → chunk-JTDFJWI2.js} +1 -0
  6. package/dist/{chunk-7LPTHFIL.js → chunk-M5CNKH4J.js} +55 -5
  7. package/dist/{chunk-L3LHXZD7.js → chunk-PHHKNGA3.js} +1 -1
  8. package/dist/{chunk-Q7AITQ44.js → chunk-QIXPN3OO.js} +1 -1
  9. package/dist/{chunk-PC6R6UUW.js → chunk-RK627D57.js} +36 -59
  10. package/dist/{chunk-5462YKWP.js → chunk-TFS25FIM.js} +1 -1
  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-PI47U2LT.js +96 -0
  15. package/dist/{daemon-restart-BH67ZOTE.js → daemon-restart-RMGOOGPE.js} +4 -4
  16. package/dist/daemon.js +1216 -1822
  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-FHV4NO2F.js +23 -0
  20. package/dist/{mind-BIDOF65R.js → mind-BTXR5B3C.js} +13 -5
  21. package/dist/{mind-manager-3V2NXX4I.js → mind-manager-KMY4GA2J.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-CUBJ4PKS.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-2TMQ65E4.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-Z5JRG2M2.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-LKABEJSA.js} +11 -3
  36. package/dist/web-assets/assets/index-CZ26vsyY.js +69 -0
  37. package/dist/web-assets/assets/index-DyyAvJwW.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/file-handler.ts +6 -1
  43. package/templates/_base/src/lib/logger.ts +68 -23
  44. package/templates/_base/src/lib/startup.ts +12 -3
  45. package/templates/claude/src/agent.ts +150 -29
  46. package/templates/claude/src/lib/hooks/pre-compact.ts +18 -4
  47. package/templates/claude/src/lib/message-channel.ts +6 -0
  48. package/templates/claude/src/lib/stream-consumer.ts +17 -1
  49. package/templates/claude/src/server.ts +3 -1
  50. package/templates/pi/home/.config/config.json.tmpl +4 -1
  51. package/templates/pi/src/agent.ts +87 -0
  52. package/templates/pi/src/lib/content.ts +18 -3
  53. package/templates/pi/src/lib/event-handler.ts +22 -2
  54. package/templates/pi/src/server.ts +3 -1
  55. package/dist/chunk-OGZYB5GL.js +0 -847
  56. package/dist/web-assets/assets/index-BR3gtK3E.css +0 -1
  57. package/dist/web-assets/assets/index-CWmrZRQd.js +0 -64
  58. /package/dist/{shared-DCQ2UXOM.js → shared-2OGT3NSL.js} +0 -0
package/dist/daemon.js CHANGED
@@ -7,62 +7,79 @@ 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,
41
+ publish2,
36
42
  publishTypingForChannels,
37
- subscribe
38
- } from "./chunk-OGZYB5GL.js";
43
+ recordInbound,
44
+ startMindFull,
45
+ stopAllWatchers,
46
+ stopMindFull,
47
+ subscribe as subscribe2,
48
+ subscribe2 as subscribe3
49
+ } from "./chunk-ISWZ6QUK.js";
39
50
  import {
40
- applyInitFiles,
41
- composeTemplate,
42
- computeTemplateHash,
43
- copyTemplateToDir,
44
- findTemplatesRoot,
45
- listFiles
46
- } from "./chunk-AKPFNL7L.js";
51
+ readSystemsConfig
52
+ } from "./chunk-HFCBO2GL.js";
47
53
  import {
48
54
  getActiveMinds,
49
- markIdle,
50
55
  onMindEvent,
51
56
  stopAll
52
57
  } from "./chunk-HGCDWKSP.js";
53
58
  import {
54
59
  broadcast,
55
- publish as publish2,
56
- subscribe as subscribe2
60
+ subscribe
57
61
  } from "./chunk-A4S7H6G6.js";
62
+ import {
63
+ PROMPT_DEFAULTS,
64
+ PROMPT_KEYS,
65
+ RotatingLog,
66
+ getMindManager,
67
+ getMindPromptDefaults,
68
+ getPrompt,
69
+ getPromptIfCustom,
70
+ initMindManager,
71
+ substitute
72
+ } from "./chunk-M5CNKH4J.js";
58
73
  import {
59
74
  findOpenClawSession,
60
75
  importOpenClawConnectors,
61
76
  importPiSession,
62
- parseNameFromIdentity,
77
+ parseNameFromIdentity
78
+ } from "./chunk-RK627D57.js";
79
+ import {
63
80
  readVoluteConfig,
64
81
  writeVoluteConfig
65
- } from "./chunk-PC6R6UUW.js";
82
+ } from "./chunk-XLC342FO.js";
66
83
  import {
67
84
  loadMergedEnv,
68
85
  mindEnvPath,
@@ -88,7 +105,7 @@ import {
88
105
  syncBuiltinSkills,
89
106
  uninstallSkill,
90
107
  updateSkill
91
- } from "./chunk-5462YKWP.js";
108
+ } from "./chunk-TFS25FIM.js";
92
109
  import {
93
110
  activity,
94
111
  conversationParticipants,
@@ -109,7 +126,7 @@ import {
109
126
  exec,
110
127
  gitExec,
111
128
  resolveVoluteBin
112
- } from "./chunk-IPJXU366.js";
129
+ } from "./chunk-JTDFJWI2.js";
113
130
  import {
114
131
  chownMindDir,
115
132
  createMindUser,
@@ -134,7 +151,6 @@ import {
134
151
  addMind,
135
152
  addVariant,
136
153
  checkHealth,
137
- daemonLoopback,
138
154
  ensureVoluteHome,
139
155
  findMind,
140
156
  findVariant,
@@ -163,1178 +179,15 @@ import {
163
179
 
164
180
  // src/daemon.ts
165
181
  import { randomBytes as randomBytes2 } from "crypto";
166
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
182
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
167
183
  import { homedir as homedir2 } from "os";
168
- import { resolve as resolve22 } from "path";
184
+ import { resolve as resolve17 } from "path";
169
185
  import { format } from "util";
170
186
 
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
187
  // src/lib/migrate-agents-to-minds.ts
1335
188
  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";
189
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
190
+ import { resolve } from "path";
1338
191
  var TAG = "[migrate]";
1339
192
  function log(msg) {
1340
193
  console.error(`${TAG} ${msg}`);
@@ -1355,19 +208,19 @@ function bridgeEnvVar() {
1355
208
  }
1356
209
  }
1357
210
  function migrateRegistry(home) {
1358
- const oldPath = resolve6(home, "agents.json");
1359
- const newPath = resolve6(home, "minds.json");
1360
- if (!existsSync5(oldPath) || existsSync5(newPath)) {
211
+ const oldPath = resolve(home, "agents.json");
212
+ const newPath = resolve(home, "minds.json");
213
+ if (!existsSync(oldPath) || existsSync(newPath)) {
1361
214
  return readNamesFromRegistry(newPath);
1362
215
  }
1363
- const raw = readFileSync4(oldPath, "utf-8");
216
+ const raw = readFileSync(oldPath, "utf-8");
1364
217
  const entries = JSON.parse(raw);
1365
218
  for (const entry of entries) {
1366
219
  if (entry.stage === "mind") {
1367
220
  entry.stage = "sprouted";
1368
221
  }
1369
222
  }
1370
- writeFileSync3(newPath, `${JSON.stringify(entries, null, 2)}
223
+ writeFileSync(newPath, `${JSON.stringify(entries, null, 2)}
1371
224
  `);
1372
225
  try {
1373
226
  renameSync(oldPath, `${oldPath}.bak`);
@@ -1377,9 +230,9 @@ function migrateRegistry(home) {
1377
230
  return entries.map((e) => e.name);
1378
231
  }
1379
232
  function readNamesFromRegistry(path) {
1380
- if (!existsSync5(path)) return [];
233
+ if (!existsSync(path)) return [];
1381
234
  try {
1382
- const entries = JSON.parse(readFileSync4(path, "utf-8"));
235
+ const entries = JSON.parse(readFileSync(path, "utf-8"));
1383
236
  return entries.map((e) => e.name);
1384
237
  } catch {
1385
238
  return [];
@@ -1387,9 +240,9 @@ function readNamesFromRegistry(path) {
1387
240
  }
1388
241
  function migrateMindsDirectory(home) {
1389
242
  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)) {
243
+ const oldDir = resolve(home, "agents");
244
+ const newDir = resolve(home, "minds");
245
+ if (existsSync(oldDir) && !existsSync(newDir)) {
1393
246
  try {
1394
247
  renameSync(oldDir, newDir);
1395
248
  log("renamed agents/ \u2192 minds/");
@@ -1400,10 +253,10 @@ function migrateMindsDirectory(home) {
1400
253
  }
1401
254
  function migrateLogFiles(home, names) {
1402
255
  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)) {
256
+ const logsDir = resolve(home, "state", name, "logs");
257
+ const oldLog = resolve(logsDir, "agent.log");
258
+ const newLog = resolve(logsDir, "mind.log");
259
+ if (existsSync(oldLog) && !existsSync(newLog)) {
1407
260
  try {
1408
261
  renameSync(oldLog, newLog);
1409
262
  log(`renamed ${name} agent.log \u2192 mind.log`);
@@ -1453,12 +306,12 @@ function migrateLinuxUsers(names) {
1453
306
  }
1454
307
  function migrateProfileScript() {
1455
308
  const profilePath = "/etc/profile.d/volute.sh";
1456
- if (!existsSync5(profilePath)) return;
309
+ if (!existsSync(profilePath)) return;
1457
310
  try {
1458
- const content = readFileSync4(profilePath, "utf-8");
311
+ const content = readFileSync(profilePath, "utf-8");
1459
312
  if (!content.includes("VOLUTE_AGENTS_DIR")) return;
1460
313
  const updated = content.replace(/VOLUTE_AGENTS_DIR/g, "VOLUTE_MINDS_DIR");
1461
- writeFileSync3(profilePath, updated);
314
+ writeFileSync(profilePath, updated);
1462
315
  log("updated /etc/profile.d/volute.sh: VOLUTE_AGENTS_DIR \u2192 VOLUTE_MINDS_DIR");
1463
316
  } catch (err) {
1464
317
  log(`failed to update profile script: ${err}`);
@@ -1466,37 +319,37 @@ function migrateProfileScript() {
1466
319
  }
1467
320
 
1468
321
  // 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";
322
+ import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, renameSync as renameSync2 } from "fs";
323
+ import { resolve as resolve2 } from "path";
1471
324
  function migrateDotVoluteDir(name) {
1472
325
  const dir = mindDir(name);
1473
- const oldDir = resolve7(dir, ".volute");
1474
- const newDir = resolve7(dir, ".mind");
1475
- if (existsSync6(oldDir) && !existsSync6(newDir)) {
326
+ const oldDir = resolve2(dir, ".volute");
327
+ const newDir = resolve2(dir, ".mind");
328
+ if (existsSync2(oldDir) && !existsSync2(newDir)) {
1476
329
  renameSync2(oldDir, newDir);
1477
- } else if (existsSync6(oldDir) && existsSync6(newDir)) {
330
+ } else if (existsSync2(oldDir) && existsSync2(newDir)) {
1478
331
  console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
1479
332
  }
1480
333
  }
1481
334
  function migrateMindState(name) {
1482
- const src = resolve7(mindDir(name), ".mind");
1483
- if (!existsSync6(src)) return;
335
+ const src = resolve2(mindDir(name), ".mind");
336
+ if (!existsSync2(src)) return;
1484
337
  const dest = stateDir(name);
1485
- mkdirSync3(dest, { recursive: true });
338
+ mkdirSync(dest, { recursive: true });
1486
339
  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)) {
340
+ const srcPath = resolve2(src, file);
341
+ const destPath = resolve2(dest, file);
342
+ if (existsSync2(srcPath) && !existsSync2(destPath)) {
1490
343
  copyFileSync(srcPath, destPath);
1491
344
  }
1492
345
  }
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)) {
346
+ const srcLogs = resolve2(src, "logs");
347
+ const destLogs = resolve2(dest, "logs");
348
+ if (existsSync2(srcLogs) && !existsSync2(destLogs)) {
349
+ mkdirSync(destLogs, { recursive: true });
350
+ for (const file of readdirSync(srcLogs)) {
1498
351
  try {
1499
- copyFileSync(resolve7(srcLogs, file), resolve7(destLogs, file));
352
+ copyFileSync(resolve2(srcLogs, file), resolve2(destLogs, file));
1500
353
  } catch (err) {
1501
354
  console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
1502
355
  }
@@ -1722,13 +575,13 @@ var authMiddleware = createMiddleware(async (c, next) => {
1722
575
  });
1723
576
 
1724
577
  // src/web/server.ts
1725
- import { existsSync as existsSync15 } from "fs";
578
+ import { existsSync as existsSync12 } from "fs";
1726
579
  import { readFile as readFile3, stat as stat3 } from "fs/promises";
1727
- import { dirname as dirname2, extname as extname3, resolve as resolve21 } from "path";
580
+ import { dirname, extname as extname3, resolve as resolve16 } from "path";
1728
581
  import { serve } from "@hono/node-server";
1729
582
 
1730
583
  // src/web/app.ts
1731
- import { Hono as Hono25 } from "hono";
584
+ import { Hono as Hono28 } from "hono";
1732
585
  import { bodyLimit } from "hono/body-limit";
1733
586
  import { csrf } from "hono/csrf";
1734
587
  import { HTTPException } from "hono/http-exception";
@@ -1740,7 +593,7 @@ import { streamSSE } from "hono/streaming";
1740
593
 
1741
594
  // src/lib/events/conversations.ts
1742
595
  import { randomUUID } from "crypto";
1743
- import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
596
+ import { and as and2, desc, eq as eq3, inArray, isNull, lt as lt2, sql } from "drizzle-orm";
1744
597
  async function createConversation(mindName, channel, opts) {
1745
598
  const db = await getDb();
1746
599
  const id = randomUUID();
@@ -1766,6 +619,11 @@ async function createConversation(mindName, channel, opts) {
1766
619
  );
1767
620
  }
1768
621
  });
622
+ fireWebhook({
623
+ event: "conversation_created",
624
+ mind: mindName ?? "",
625
+ data: { id, mindName, channel, type, name, title: opts?.title ?? null }
626
+ });
1769
627
  return {
1770
628
  id,
1771
629
  mind_name: mindName,
@@ -1858,7 +716,7 @@ async function addMessage(conversationId, role, senderName, content) {
1858
716
  content,
1859
717
  created_at: result.created_at
1860
718
  };
1861
- publish(conversationId, {
719
+ publish2(conversationId, {
1862
720
  type: "message",
1863
721
  id: msg.id,
1864
722
  role: msg.role,
@@ -1866,21 +724,50 @@ async function addMessage(conversationId, role, senderName, content) {
1866
724
  content: msg.content,
1867
725
  createdAt: msg.created_at
1868
726
  });
727
+ const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq3(conversations.id, conversationId)).get();
728
+ fireWebhook({
729
+ event: "message_created",
730
+ mind: conv?.mind_name ?? "",
731
+ data: {
732
+ conversationId,
733
+ messageId: result.id,
734
+ role,
735
+ senderName,
736
+ content: content.filter((b) => b.type !== "image"),
737
+ createdAt: result.created_at
738
+ }
739
+ });
1869
740
  return msg;
1870
741
  }
1871
742
  async function getMessages(conversationId) {
1872
743
  const db = await getDb();
1873
744
  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
- });
745
+ return rows.map(parseMessageRow);
746
+ }
747
+ async function getMessagesPaginated(conversationId, opts) {
748
+ const db = await getDb();
749
+ const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
750
+ const conditions = [eq3(messages.conversation_id, conversationId)];
751
+ if (opts?.before != null) {
752
+ conditions.push(lt2(messages.id, opts.before));
753
+ }
754
+ const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
755
+ const hasMore = rows.length > limit;
756
+ const page = rows.slice(0, limit).reverse();
757
+ return {
758
+ messages: page.map(parseMessageRow),
759
+ hasMore
760
+ };
761
+ }
762
+ function parseMessageRow(row) {
763
+ let content;
764
+ try {
765
+ const parsed = JSON.parse(row.content);
766
+ content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
767
+ } catch {
768
+ content = [{ type: "text", text: row.content }];
769
+ }
770
+ return { ...row, role: row.role, content };
1884
771
  }
1885
772
  async function listConversationsWithParticipants(userId) {
1886
773
  const convs = await listConversationsForUser(userId);
@@ -2022,7 +909,7 @@ var app = new Hono().get("/events", async (c) => {
2022
909
  activeMinds: getActiveMinds()
2023
910
  })
2024
911
  });
2025
- const unsubActivity = subscribe2((event) => {
912
+ const unsubActivity = subscribe((event) => {
2026
913
  stream.writeSSE({
2027
914
  data: JSON.stringify({ event: "activity", ...event })
2028
915
  }).catch((err) => {
@@ -2031,7 +918,7 @@ var app = new Hono().get("/events", async (c) => {
2031
918
  });
2032
919
  cleanups.push(unsubActivity);
2033
920
  for (const conv of conversations2) {
2034
- const unsubConv = subscribe(conv.id, (event) => {
921
+ const unsubConv = subscribe3(conv.id, (event) => {
2035
922
  stream.writeSSE({
2036
923
  data: JSON.stringify({ event: "conversation", conversationId: conv.id, ...event })
2037
924
  }).catch((err) => {
@@ -2046,8 +933,8 @@ var app = new Hono().get("/events", async (c) => {
2046
933
  });
2047
934
  }, 15e3);
2048
935
  cleanups.push(() => clearInterval(keepAlive));
2049
- await new Promise((resolve23) => {
2050
- stream.onAbort(() => resolve23());
936
+ await new Promise((resolve18) => {
937
+ stream.onAbort(() => resolve18());
2051
938
  });
2052
939
  } finally {
2053
940
  for (const cleanup of cleanups) {
@@ -2609,16 +1496,16 @@ __export(volute_exports, {
2609
1496
  read: () => read4,
2610
1497
  send: () => send4
2611
1498
  });
2612
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
2613
- import { resolve as resolve8 } from "path";
1499
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1500
+ import { resolve as resolve3 } from "path";
2614
1501
  function getDaemonConfig() {
2615
- const configPath2 = resolve8(voluteHome(), "daemon.json");
2616
- if (!existsSync7(configPath2)) {
1502
+ const configPath2 = resolve3(voluteHome(), "daemon.json");
1503
+ if (!existsSync3(configPath2)) {
2617
1504
  throw new Error("Volute daemon is not running");
2618
1505
  }
2619
1506
  let config;
2620
1507
  try {
2621
- config = JSON.parse(readFileSync5(configPath2, "utf-8"));
1508
+ config = JSON.parse(readFileSync2(configPath2, "utf-8"));
2622
1509
  } catch (err) {
2623
1510
  throw new Error(`Failed to parse ${configPath2}: ${err}`);
2624
1511
  }
@@ -3055,14 +1942,14 @@ var sharedEnvApp = new Hono5().get("/", (c) => {
3055
1942
  var env_default = app5;
3056
1943
 
3057
1944
  // src/web/api/file-sharing.ts
3058
- import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
3059
- import { resolve as resolve10 } from "path";
1945
+ import { readFileSync as readFileSync4, statSync } from "fs";
1946
+ import { resolve as resolve5 } from "path";
3060
1947
  import { Hono as Hono6 } from "hono";
3061
1948
 
3062
1949
  // src/lib/file-sharing.ts
3063
1950
  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";
1951
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1952
+ import { basename, join, normalize, resolve as resolve4 } from "path";
3066
1953
  function validateFilePath(filePath) {
3067
1954
  if (!filePath) return "File path is required";
3068
1955
  const normalized = normalize(filePath);
@@ -3075,13 +1962,13 @@ function validateFilePath(filePath) {
3075
1962
  return null;
3076
1963
  }
3077
1964
  function configPath(dir) {
3078
- return resolve9(dir, "home", ".config", "file-sharing.json");
1965
+ return resolve4(dir, "home", ".config", "file-sharing.json");
3079
1966
  }
3080
1967
  function readFileSharingConfig(dir) {
3081
1968
  const p = configPath(dir);
3082
- if (!existsSync8(p)) return {};
1969
+ if (!existsSync4(p)) return {};
3083
1970
  try {
3084
- return JSON.parse(readFileSync6(p, "utf-8"));
1971
+ return JSON.parse(readFileSync3(p, "utf-8"));
3085
1972
  } catch (err) {
3086
1973
  console.warn(`[file-sharing] failed to parse config at ${p}:`, err);
3087
1974
  return {};
@@ -3089,8 +1976,8 @@ function readFileSharingConfig(dir) {
3089
1976
  }
3090
1977
  function writeFileSharingConfig(dir, config) {
3091
1978
  const p = configPath(dir);
3092
- mkdirSync4(resolve9(p, ".."), { recursive: true });
3093
- writeFileSync4(p, `${JSON.stringify(config, null, 2)}
1979
+ mkdirSync2(resolve4(p, ".."), { recursive: true });
1980
+ writeFileSync2(p, `${JSON.stringify(config, null, 2)}
3094
1981
  `);
3095
1982
  }
3096
1983
  function isTrustedSender(dir, sender) {
@@ -3113,7 +2000,7 @@ function removeTrust(dir, sender) {
3113
2000
  writeFileSharingConfig(dir, config);
3114
2001
  }
3115
2002
  function pendingDir(receiver) {
3116
- return resolve9(stateDir(receiver), "pending-files");
2003
+ return resolve4(stateDir(receiver), "pending-files");
3117
2004
  }
3118
2005
  function validateId(id) {
3119
2006
  if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
@@ -3132,8 +2019,8 @@ function stageFile(receiver, sender, filename, content, originalPath) {
3132
2019
  throw new Error("Invalid sender name");
3133
2020
  }
3134
2021
  const id = generateId(sender);
3135
- const dir = resolve9(pendingDir(receiver), id);
3136
- mkdirSync4(dir, { recursive: true });
2022
+ const dir = resolve4(pendingDir(receiver), id);
2023
+ mkdirSync2(dir, { recursive: true });
3137
2024
  const metadata = {
3138
2025
  id,
3139
2026
  sender,
@@ -3142,22 +2029,22 @@ function stageFile(receiver, sender, filename, content, originalPath) {
3142
2029
  size: content.length,
3143
2030
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3144
2031
  };
3145
- writeFileSync4(resolve9(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
2032
+ writeFileSync2(resolve4(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
3146
2033
  `);
3147
- writeFileSync4(resolve9(dir, "data"), content);
2034
+ writeFileSync2(resolve4(dir, "data"), content);
3148
2035
  return { id };
3149
2036
  }
3150
2037
  function listPending(receiver) {
3151
2038
  const dir = pendingDir(receiver);
3152
- if (!existsSync8(dir)) return [];
3153
- const entries = readdirSync3(dir, { withFileTypes: true });
2039
+ if (!existsSync4(dir)) return [];
2040
+ const entries = readdirSync2(dir, { withFileTypes: true });
3154
2041
  const result = [];
3155
2042
  for (const entry of entries) {
3156
2043
  if (!entry.isDirectory()) continue;
3157
- const metaPath = resolve9(dir, entry.name, "metadata.json");
3158
- if (!existsSync8(metaPath)) continue;
2044
+ const metaPath = resolve4(dir, entry.name, "metadata.json");
2045
+ if (!existsSync4(metaPath)) continue;
3159
2046
  try {
3160
- result.push(JSON.parse(readFileSync6(metaPath, "utf-8")));
2047
+ result.push(JSON.parse(readFileSync3(metaPath, "utf-8")));
3161
2048
  } catch (err) {
3162
2049
  console.warn(`[file-sharing] skipping malformed pending entry ${entry.name}:`, err);
3163
2050
  }
@@ -3166,10 +2053,10 @@ function listPending(receiver) {
3166
2053
  }
3167
2054
  function getPending(receiver, id) {
3168
2055
  validateId(id);
3169
- const metaPath = resolve9(pendingDir(receiver), id, "metadata.json");
3170
- if (!existsSync8(metaPath)) return null;
2056
+ const metaPath = resolve4(pendingDir(receiver), id, "metadata.json");
2057
+ if (!existsSync4(metaPath)) return null;
3171
2058
  try {
3172
- return JSON.parse(readFileSync6(metaPath, "utf-8"));
2059
+ return JSON.parse(readFileSync3(metaPath, "utf-8"));
3173
2060
  } catch (err) {
3174
2061
  console.warn(`[file-sharing] failed to read pending metadata for ${id}:`, err);
3175
2062
  return null;
@@ -3184,27 +2071,27 @@ function deliverFile(receiverDir, sender, filename, content, inboxPath) {
3184
2071
  if (sender.includes("/") || sender.includes("\\")) {
3185
2072
  throw new Error("Invalid sender name");
3186
2073
  }
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));
2074
+ const destDir = resolve4(receiverDir, "home", inbox, sender);
2075
+ mkdirSync2(destDir, { recursive: true });
2076
+ const destPath = resolve4(destDir, basename(filename));
2077
+ writeFileSync2(destPath, content);
2078
+ return join(inbox, sender, basename(filename));
3192
2079
  }
3193
2080
  function acceptPending(receiver, id, receiverDir) {
3194
2081
  const meta = getPending(receiver, id);
3195
2082
  if (!meta) throw new Error(`Pending file not found: ${id}`);
3196
- const dataPath = resolve9(pendingDir(receiver), id, "data");
3197
- const content = readFileSync6(dataPath);
2083
+ const dataPath = resolve4(pendingDir(receiver), id, "data");
2084
+ const content = readFileSync3(dataPath);
3198
2085
  const config = readFileSharingConfig(receiverDir);
3199
2086
  const inboxPath = config.inboxPath ?? "inbox";
3200
2087
  const destPath = deliverFile(receiverDir, meta.sender, meta.filename, content, inboxPath);
3201
- rmSync(resolve9(pendingDir(receiver), id), { recursive: true });
2088
+ rmSync(resolve4(pendingDir(receiver), id), { recursive: true });
3202
2089
  return { sender: meta.sender, filename: meta.filename, destPath };
3203
2090
  }
3204
2091
  function rejectPending(receiver, id) {
3205
2092
  const meta = getPending(receiver, id);
3206
2093
  if (!meta) throw new Error(`Pending file not found: ${id}`);
3207
- rmSync(resolve9(pendingDir(receiver), id), { recursive: true });
2094
+ rmSync(resolve4(pendingDir(receiver), id), { recursive: true });
3208
2095
  return { sender: meta.sender, filename: meta.filename };
3209
2096
  }
3210
2097
  function formatFileSize(bytes) {
@@ -3245,9 +2132,9 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
3245
2132
  const pathErr = validateFilePath(body.filePath);
3246
2133
  if (pathErr) return c.json({ error: pathErr }, 400);
3247
2134
  const senderDir = mindDir(senderName);
3248
- const filePath = resolve10(senderDir, "home", body.filePath);
2135
+ const filePath = resolve5(senderDir, "home", body.filePath);
3249
2136
  const MAX_FILE_SIZE = 50 * 1024 * 1024;
3250
- const stat4 = statSync2(filePath, { throwIfNoEntry: false });
2137
+ const stat4 = statSync(filePath, { throwIfNoEntry: false });
3251
2138
  if (!stat4) return c.json({ error: `File not found: ${body.filePath}` }, 404);
3252
2139
  if (stat4.size > MAX_FILE_SIZE) {
3253
2140
  return c.json(
@@ -3259,7 +2146,7 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
3259
2146
  }
3260
2147
  let content;
3261
2148
  try {
3262
- content = readFileSync7(filePath);
2149
+ content = readFileSync4(filePath);
3263
2150
  } catch {
3264
2151
  return c.json({ error: `File not found: ${body.filePath}` }, 404);
3265
2152
  }
@@ -3361,9 +2248,9 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
3361
2248
  var file_sharing_default = app6;
3362
2249
 
3363
2250
  // src/web/api/files.ts
3364
- import { existsSync as existsSync9 } from "fs";
2251
+ import { existsSync as existsSync5 } from "fs";
3365
2252
  import { readdir, readFile, realpath, stat } from "fs/promises";
3366
- import { extname, resolve as resolve11 } from "path";
2253
+ import { extname, resolve as resolve6 } from "path";
3367
2254
  import { Hono as Hono7 } from "hono";
3368
2255
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
3369
2256
  var AVATAR_MIME = {
@@ -3384,8 +2271,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
3384
2271
  const ext = extname(config.avatar).toLowerCase();
3385
2272
  const mime = AVATAR_MIME[ext];
3386
2273
  if (!mime) return c.json({ error: "Invalid avatar extension" }, 400);
3387
- const homeDir = resolve11(dir, "home");
3388
- const avatarPath = resolve11(homeDir, config.avatar);
2274
+ const homeDir = resolve6(dir, "home");
2275
+ const avatarPath = resolve6(homeDir, config.avatar);
3389
2276
  if (!avatarPath.startsWith(`${homeDir}/`)) return c.json({ error: "Invalid avatar path" }, 400);
3390
2277
  let realAvatarPath;
3391
2278
  try {
@@ -3414,8 +2301,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
3414
2301
  const entry = findMind(name);
3415
2302
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3416
2303
  const dir = mindDir(name);
3417
- const homeDir = resolve11(dir, "home");
3418
- if (!existsSync9(homeDir)) return c.json({ error: "Home directory missing" }, 404);
2304
+ const homeDir = resolve6(dir, "home");
2305
+ if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
3419
2306
  const allFiles = await readdir(homeDir);
3420
2307
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
3421
2308
  return c.json(files);
@@ -3428,8 +2315,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
3428
2315
  const entry = findMind(name);
3429
2316
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3430
2317
  const dir = mindDir(name);
3431
- const filePath = resolve11(dir, "home", filename);
3432
- if (!existsSync9(filePath)) {
2318
+ const filePath = resolve6(dir, "home", filename);
2319
+ if (!existsSync5(filePath)) {
3433
2320
  return c.json({ error: "File not found" }, 404);
3434
2321
  }
3435
2322
  const content = await readFile(filePath, "utf-8");
@@ -3442,19 +2329,19 @@ import { Hono as Hono8 } from "hono";
3442
2329
 
3443
2330
  // src/lib/identity.ts
3444
2331
  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";
2332
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2333
+ import { resolve as resolve7 } from "path";
3447
2334
  function generateIdentity(mindDir2) {
3448
- const identityDir = resolve12(mindDir2, ".mind/identity");
3449
- mkdirSync5(identityDir, { recursive: true });
2335
+ const identityDir = resolve7(mindDir2, ".mind/identity");
2336
+ mkdirSync3(identityDir, { recursive: true });
3450
2337
  const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
3451
2338
  publicKeyEncoding: { type: "spki", format: "pem" },
3452
2339
  privateKeyEncoding: { type: "pkcs8", format: "pem" }
3453
2340
  });
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 });
2341
+ const privatePath = resolve7(identityDir, "private.pem");
2342
+ const publicPath = resolve7(identityDir, "public.pem");
2343
+ writeFileSync3(privatePath, privateKey, { mode: 384 });
2344
+ writeFileSync3(publicPath, publicKey, { mode: 420 });
3458
2345
  const config = readVoluteConfig(mindDir2) ?? {};
3459
2346
  config.identity = {
3460
2347
  privateKey: ".mind/identity/private.pem",
@@ -3467,17 +2354,17 @@ function getPrivateKey(mindDir2) {
3467
2354
  const config = readVoluteConfig(mindDir2);
3468
2355
  const relPath = config?.identity?.privateKey;
3469
2356
  if (!relPath) return null;
3470
- const fullPath = resolve12(mindDir2, relPath);
3471
- if (!existsSync10(fullPath)) return null;
3472
- return readFileSync8(fullPath, "utf-8");
2357
+ const fullPath = resolve7(mindDir2, relPath);
2358
+ if (!existsSync6(fullPath)) return null;
2359
+ return readFileSync5(fullPath, "utf-8");
3473
2360
  }
3474
2361
  function getPublicKey(mindDir2) {
3475
2362
  const config = readVoluteConfig(mindDir2);
3476
2363
  const relPath = config?.identity?.publicKey;
3477
2364
  if (!relPath) return null;
3478
- const fullPath = resolve12(mindDir2, relPath);
3479
- if (!existsSync10(fullPath)) return null;
3480
- return readFileSync8(fullPath, "utf-8");
2365
+ const fullPath = resolve7(mindDir2, relPath);
2366
+ if (!existsSync6(fullPath)) return null;
2367
+ return readFileSync5(fullPath, "utf-8");
3481
2368
  }
3482
2369
  function getFingerprint(publicKeyPem) {
3483
2370
  return createHash("sha256").update(publicKeyPem).digest("hex");
@@ -3529,21 +2416,21 @@ var app8 = new Hono8().get("/:fingerprint", (c) => {
3529
2416
  var keys_default = app8;
3530
2417
 
3531
2418
  // 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";
2419
+ import { spawn } from "child_process";
2420
+ import { existsSync as existsSync7 } from "fs";
2421
+ import { resolve as resolve8 } from "path";
3535
2422
  import { Hono as Hono9 } from "hono";
3536
2423
  import { streamSSE as streamSSE2 } from "hono/streaming";
3537
2424
  var app9 = new Hono9().get("/:name/logs", async (c) => {
3538
2425
  const name = c.req.param("name");
3539
2426
  const entry = findMind(name);
3540
2427
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3541
- const logFile = resolve13(stateDir(name), "logs", "mind.log");
3542
- if (!existsSync11(logFile)) {
2428
+ const logFile = resolve8(stateDir(name), "logs", "mind.log");
2429
+ if (!existsSync7(logFile)) {
3543
2430
  return c.json({ error: "No log file found" }, 404);
3544
2431
  }
3545
2432
  return streamSSE2(c, async (stream) => {
3546
- const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
2433
+ const tail = spawn("tail", ["-n", "200", "-f", logFile]);
3547
2434
  const onData = (data) => {
3548
2435
  const lines = data.toString().split("\n");
3549
2436
  for (const line of lines) {
@@ -3557,28 +2444,28 @@ var app9 = new Hono9().get("/:name/logs", async (c) => {
3557
2444
  stream.onAbort(() => {
3558
2445
  tail.kill();
3559
2446
  });
3560
- await new Promise((resolve23) => {
3561
- tail.on("exit", resolve23);
3562
- stream.onAbort(resolve23);
2447
+ await new Promise((resolve18) => {
2448
+ tail.on("exit", resolve18);
2449
+ stream.onAbort(resolve18);
3563
2450
  });
3564
2451
  });
3565
2452
  }).get("/:name/logs/tail", async (c) => {
3566
2453
  const name = c.req.param("name");
3567
2454
  const entry = findMind(name);
3568
2455
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3569
- const logFile = resolve13(stateDir(name), "logs", "mind.log");
3570
- if (!existsSync11(logFile)) {
2456
+ const logFile = resolve8(stateDir(name), "logs", "mind.log");
2457
+ if (!existsSync7(logFile)) {
3571
2458
  return c.json({ error: "No log file found" }, 404);
3572
2459
  }
3573
2460
  const nParam = parseInt(c.req.query("n") ?? "50", 10);
3574
2461
  const n = Number.isFinite(nParam) && nParam > 0 ? Math.min(nParam, 1e4) : 50;
3575
- const tail = spawn2("tail", ["-n", String(n), logFile]);
2462
+ const tail = spawn("tail", ["-n", String(n), logFile]);
3576
2463
  let output = "";
3577
2464
  tail.stdout.on("data", (data) => {
3578
2465
  output += data.toString();
3579
2466
  });
3580
- await new Promise((resolve23) => {
3581
- tail.on("exit", resolve23);
2467
+ await new Promise((resolve18) => {
2468
+ tail.on("exit", resolve18);
3582
2469
  });
3583
2470
  return c.text(output);
3584
2471
  });
@@ -3668,33 +2555,33 @@ var mind_skills_default = app10;
3668
2555
  // src/web/api/minds.ts
3669
2556
  import {
3670
2557
  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
2558
+ existsSync as existsSync9,
2559
+ mkdirSync as mkdirSync5,
2560
+ readdirSync as readdirSync4,
2561
+ readFileSync as readFileSync8,
2562
+ rmSync as rmSync3,
2563
+ writeFileSync as writeFileSync6
3677
2564
  } from "fs";
3678
- import { resolve as resolve16 } from "path";
2565
+ import { resolve as resolve11 } from "path";
3679
2566
  import { zValidator as zValidator3 } from "@hono/zod-validator";
3680
2567
  import { and as and3, desc as desc3, eq as eq4, sql as sql2 } from "drizzle-orm";
3681
2568
  import { Hono as Hono11 } from "hono";
3682
2569
  import { z as z3 } from "zod";
3683
2570
 
3684
2571
  // src/lib/consolidate.ts
3685
- import { readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
3686
- import { resolve as resolve14 } from "path";
2572
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2573
+ import { resolve as resolve9 } from "path";
3687
2574
  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");
2575
+ const soulPath = resolve9(mindDir2, "home/SOUL.md");
2576
+ const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
2577
+ const memoryDir = resolve9(mindDir2, "home/memory");
2578
+ const soul = readFileSync6(soulPath, "utf-8");
3692
2579
  const logs = [];
3693
2580
  try {
3694
- const files = readdirSync4(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2581
+ const files = readdirSync3(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
3695
2582
  for (const filename of files) {
3696
2583
  const date = filename.replace(".md", "");
3697
- const content2 = readFileSync9(resolve14(memoryDir, filename), "utf-8").trim();
2584
+ const content2 = readFileSync6(resolve9(memoryDir, filename), "utf-8").trim();
3698
2585
  if (content2) {
3699
2586
  logs.push(`### ${date}
3700
2587
 
@@ -3744,7 +2631,7 @@ ${content2}`);
3744
2631
  const data = await res.json();
3745
2632
  const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
3746
2633
  if (content) {
3747
- writeFileSync6(memoryPath, `${content}
2634
+ writeFileSync4(memoryPath, `${content}
3748
2635
  `);
3749
2636
  console.log("MEMORY.md created successfully.");
3750
2637
  } else {
@@ -3754,11 +2641,11 @@ ${content2}`);
3754
2641
 
3755
2642
  // src/lib/convert-session.ts
3756
2643
  import { randomUUID as randomUUID2 } from "crypto";
3757
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
2644
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
3758
2645
  import { homedir } from "os";
3759
- import { resolve as resolve15 } from "path";
2646
+ import { resolve as resolve10 } from "path";
3760
2647
  function convertSession(opts) {
3761
- const lines = readFileSync10(opts.sessionPath, "utf-8").trim().split("\n");
2648
+ const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
3762
2649
  const sessionId = randomUUID2();
3763
2650
  const idMap = /* @__PURE__ */ new Map();
3764
2651
  const messages2 = [];
@@ -3872,10 +2759,10 @@ function convertSession(opts) {
3872
2759
  }
3873
2760
  }
3874
2761
  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")}
2762
+ const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
2763
+ mkdirSync4(sdkDir, { recursive: true });
2764
+ const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
2765
+ writeFileSync5(sdkPath, `${sdkEvents.join("\n")}
3879
2766
  `);
3880
2767
  console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
3881
2768
  return sessionId;
@@ -3926,39 +2813,53 @@ function convertAssistantContent(content) {
3926
2813
  return result;
3927
2814
  }
3928
2815
 
3929
- // src/lib/events/mind-events.ts
3930
- var subscribers = /* @__PURE__ */ new Map();
3931
- function subscribe3(mind, callback) {
3932
- let set = subscribers.get(mind);
3933
- if (!set) {
3934
- set = /* @__PURE__ */ new Set();
3935
- subscribers.set(mind, set);
3936
- }
3937
- set.add(callback);
3938
- return () => {
3939
- set.delete(callback);
3940
- if (set.size === 0) subscribers.delete(mind);
3941
- };
3942
- }
3943
- function publish3(mind, event) {
3944
- const set = subscribers.get(mind);
3945
- if (!set) return;
3946
- for (const cb of set) {
2816
+ // src/lib/variant-cleanup.ts
2817
+ import { existsSync as existsSync8, rmSync as rmSync2 } from "fs";
2818
+ async function cleanupVariant(mindName, variantName, projectRoot, variantPath, opts) {
2819
+ if (opts?.stop) {
3947
2820
  try {
3948
- cb(event);
3949
- } catch (err) {
3950
- console.error("[mind-events] subscriber threw:", err);
3951
- set.delete(cb);
3952
- if (set.size === 0) subscribers.delete(mind);
2821
+ await getMindManager().stopMind(`${mindName}@${variantName}`);
2822
+ } catch {
3953
2823
  }
3954
2824
  }
2825
+ if (existsSync8(variantPath)) {
2826
+ try {
2827
+ await gitExec(["worktree", "remove", "--force", variantPath], { cwd: projectRoot });
2828
+ } catch {
2829
+ rmSync2(variantPath, { recursive: true, force: true });
2830
+ try {
2831
+ await gitExec(["worktree", "prune"], { cwd: projectRoot });
2832
+ } catch {
2833
+ }
2834
+ }
2835
+ }
2836
+ try {
2837
+ await gitExec(["branch", "-D", variantName], { cwd: projectRoot });
2838
+ } catch {
2839
+ }
2840
+ try {
2841
+ removeVariant(mindName, variantName);
2842
+ } catch {
2843
+ }
2844
+ try {
2845
+ chownMindDir(projectRoot, mindName);
2846
+ } catch (err) {
2847
+ logger_default.error(`failed to fix ownership during variant cleanup for ${mindName}`, logger_default.errorData(err));
2848
+ }
3955
2849
  }
3956
2850
 
3957
2851
  // src/web/api/minds.ts
3958
2852
  async function getMindStatus(name, port) {
3959
2853
  const manager = getMindManager();
3960
2854
  let status = "stopped";
3961
- if (manager.isRunning(name)) {
2855
+ try {
2856
+ const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
2857
+ if (getSleepManagerIfReady()?.isSleeping(name)) {
2858
+ status = "sleeping";
2859
+ }
2860
+ } catch {
2861
+ }
2862
+ if (status !== "sleeping" && manager.isRunning(name)) {
3962
2863
  const health = await checkHealth(port);
3963
2864
  status = health.ok ? "running" : "starting";
3964
2865
  }
@@ -4010,7 +2911,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
4010
2911
  await gitExec(["commit", "-m", "initial commit"], opts);
4011
2912
  }
4012
2913
  async function updateTemplateBranch(projectRoot, template, mindName) {
4013
- const tempWorktree = resolve16(projectRoot, ".variants", "_template_update");
2914
+ const tempWorktree = resolve11(projectRoot, ".variants", "_template_update");
4014
2915
  let branchExists = false;
4015
2916
  try {
4016
2917
  await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
@@ -4021,8 +2922,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
4021
2922
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
4022
2923
  } catch {
4023
2924
  }
4024
- if (existsSync12(tempWorktree)) {
4025
- rmSync2(tempWorktree, { recursive: true, force: true });
2925
+ if (existsSync9(tempWorktree)) {
2926
+ rmSync3(tempWorktree, { recursive: true, force: true });
4026
2927
  }
4027
2928
  const templatesRoot = findTemplatesRoot();
4028
2929
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
@@ -4042,9 +2943,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
4042
2943
  });
4043
2944
  }
4044
2945
  copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
4045
- const initDir = resolve16(tempWorktree, ".init");
4046
- if (existsSync12(initDir)) {
4047
- rmSync2(initDir, { recursive: true, force: true });
2946
+ const initDir = resolve11(tempWorktree, ".init");
2947
+ if (existsSync9(initDir)) {
2948
+ rmSync3(initDir, { recursive: true, force: true });
4048
2949
  }
4049
2950
  await gitExec(["add", "-A"], { cwd: tempWorktree });
4050
2951
  try {
@@ -4057,10 +2958,10 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
4057
2958
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
4058
2959
  } catch {
4059
2960
  }
4060
- if (existsSync12(tempWorktree)) {
4061
- rmSync2(tempWorktree, { recursive: true, force: true });
2961
+ if (existsSync9(tempWorktree)) {
2962
+ rmSync3(tempWorktree, { recursive: true, force: true });
4062
2963
  }
4063
- rmSync2(composedDir, { recursive: true, force: true });
2964
+ rmSync3(composedDir, { recursive: true, force: true });
4064
2965
  }
4065
2966
  }
4066
2967
  async function mergeTemplateBranch(worktreeDir) {
@@ -4083,14 +2984,14 @@ async function mergeTemplateBranch(worktreeDir) {
4083
2984
  async function npmInstallAsMind(cwd, mindName) {
4084
2985
  if (isIsolationEnabled()) {
4085
2986
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
4086
- await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve16(cwd, "home") } });
2987
+ await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve11(cwd, "home") } });
4087
2988
  } else {
4088
2989
  await exec("npm", ["install"], { cwd });
4089
2990
  }
4090
2991
  }
4091
2992
  async function importFromArchive(c, tempDir, nameOverride, manifest) {
4092
- const extractedMindDir = resolve16(tempDir, "mind");
4093
- if (!existsSync12(extractedMindDir)) {
2993
+ const extractedMindDir = resolve11(tempDir, "mind");
2994
+ if (!existsSync9(extractedMindDir)) {
4094
2995
  return c.json({ error: "Invalid archive: missing mind/ directory" }, 400);
4095
2996
  }
4096
2997
  if (!manifest?.includes || !manifest.name || !manifest.template) {
@@ -4108,21 +3009,21 @@ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride,
4108
3009
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
4109
3010
  ensureVoluteHome();
4110
3011
  const dest = mindDir(name);
4111
- if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3012
+ if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4112
3013
  try {
4113
3014
  cpSync(extractedMindDir, dest, { recursive: true });
4114
3015
  if (!manifest.includes.identity) {
4115
3016
  generateIdentity(dest);
4116
3017
  }
4117
3018
  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"));
3019
+ mkdirSync5(state, { recursive: true });
3020
+ const channelsJson = resolve11(tempDir, "state/channels.json");
3021
+ if (existsSync9(channelsJson)) {
3022
+ cpSync(channelsJson, resolve11(state, "channels.json"));
4122
3023
  }
4123
- const envJson = resolve16(tempDir, "state/env.json");
4124
- if (existsSync12(envJson)) {
4125
- cpSync(envJson, resolve16(state, "env.json"));
3024
+ const envJson = resolve11(tempDir, "state/env.json");
3025
+ if (existsSync9(envJson)) {
3026
+ cpSync(envJson, resolve11(state, "env.json"));
4126
3027
  }
4127
3028
  const port = nextPort();
4128
3029
  addMind(name, port, manifest.stage, manifest.template);
@@ -4131,36 +3032,36 @@ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride,
4131
3032
  } catch (err) {
4132
3033
  logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4133
3034
  }
4134
- const homeDir = resolve16(dest, "home");
3035
+ const homeDir = resolve11(dest, "home");
4135
3036
  ensureVoluteGroup();
4136
3037
  createMindUser(name, homeDir);
4137
3038
  chownMindDir(dest, name);
4138
3039
  await npmInstallAsMind(dest, name);
4139
3040
  await importHistoryFromArchive(name, tempDir);
4140
3041
  importSessionsFromArchive(dest, tempDir);
4141
- if (!existsSync12(resolve16(dest, ".git"))) {
3042
+ if (!existsSync9(resolve11(dest, ".git"))) {
4142
3043
  try {
4143
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
3044
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
4144
3045
  await gitExec(["init"], { cwd: dest, mindName: name, env });
4145
3046
  await configureGitIdentity(name, { cwd: dest, mindName: name, env });
4146
3047
  await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
4147
3048
  await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
4148
3049
  } catch (err) {
4149
3050
  logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
4150
- rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
3051
+ rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
4151
3052
  }
4152
3053
  }
4153
3054
  chownMindDir(dest, name);
4154
- rmSync2(tempDir, { recursive: true, force: true });
3055
+ rmSync3(tempDir, { recursive: true, force: true });
4155
3056
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
4156
3057
  } catch (err) {
4157
- if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3058
+ if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
4158
3059
  try {
4159
3060
  removeMind(name);
4160
3061
  } catch (cleanupErr) {
4161
3062
  logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
4162
3063
  }
4163
- rmSync2(tempDir, { recursive: true, force: true });
3064
+ rmSync3(tempDir, { recursive: true, force: true });
4164
3065
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
4165
3066
  }
4166
3067
  }
@@ -4171,7 +3072,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4171
3072
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
4172
3073
  ensureVoluteHome();
4173
3074
  const dest = mindDir(name);
4174
- if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3075
+ if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4175
3076
  const templatesRoot = findTemplatesRoot();
4176
3077
  const { composedDir, manifest: templateManifest } = composeTemplate(
4177
3078
  templatesRoot,
@@ -4180,40 +3081,40 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4180
3081
  try {
4181
3082
  copyTemplateToDir(composedDir, dest, name, templateManifest);
4182
3083
  applyInitFiles(dest);
4183
- const extractedHome = resolve16(extractedMindDir, "home");
4184
- if (existsSync12(extractedHome)) {
4185
- cpSync(extractedHome, resolve16(dest, "home"), { recursive: true });
3084
+ const extractedHome = resolve11(extractedMindDir, "home");
3085
+ if (existsSync9(extractedHome)) {
3086
+ cpSync(extractedHome, resolve11(dest, "home"), { recursive: true });
4186
3087
  }
4187
- const extractedMindInternal = resolve16(extractedMindDir, ".mind");
4188
- if (existsSync12(extractedMindInternal)) {
4189
- cpSync(extractedMindInternal, resolve16(dest, ".mind"), { recursive: true });
3088
+ const extractedMindInternal = resolve11(extractedMindDir, ".mind");
3089
+ if (existsSync9(extractedMindInternal)) {
3090
+ cpSync(extractedMindInternal, resolve11(dest, ".mind"), { recursive: true });
4190
3091
  }
4191
- const identityDir = resolve16(dest, ".mind/identity");
3092
+ const identityDir = resolve11(dest, ".mind/identity");
4192
3093
  let publicKeyPem;
4193
- if (!manifest.includes.identity || !existsSync12(resolve16(identityDir, "private.pem"))) {
3094
+ if (!manifest.includes.identity || !existsSync9(resolve11(identityDir, "private.pem"))) {
4194
3095
  ({ publicKeyPem } = generateIdentity(dest));
4195
3096
  } else {
4196
- publicKeyPem = readFileSync11(resolve16(identityDir, "public.pem"), "utf-8");
3097
+ publicKeyPem = readFileSync8(resolve11(identityDir, "public.pem"), "utf-8");
4197
3098
  }
4198
- const promptsPath = resolve16(dest, "home/.config/prompts.json");
4199
- if (!existsSync12(promptsPath)) {
3099
+ const promptsPath = resolve11(dest, "home/.config/prompts.json");
3100
+ if (!existsSync9(promptsPath)) {
4200
3101
  const mindPrompts = await getMindPromptDefaults();
4201
- writeFileSync8(promptsPath, `${JSON.stringify(mindPrompts, null, 2)}
3102
+ writeFileSync6(promptsPath, `${JSON.stringify(mindPrompts, null, 2)}
4202
3103
  `);
4203
3104
  }
4204
3105
  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"));
3106
+ mkdirSync5(state, { recursive: true });
3107
+ const channelsJson = resolve11(tempDir, "state/channels.json");
3108
+ if (existsSync9(channelsJson)) {
3109
+ cpSync(channelsJson, resolve11(state, "channels.json"));
4209
3110
  }
4210
- const envJson = resolve16(tempDir, "state/env.json");
4211
- if (existsSync12(envJson)) {
4212
- cpSync(envJson, resolve16(state, "env.json"));
3111
+ const envJson = resolve11(tempDir, "state/env.json");
3112
+ if (existsSync9(envJson)) {
3113
+ cpSync(envJson, resolve11(state, "env.json"));
4213
3114
  }
4214
3115
  const port = nextPort();
4215
3116
  addMind(name, port, manifest.stage, manifest.template);
4216
- const homeDir = resolve16(dest, "home");
3117
+ const homeDir = resolve11(dest, "home");
4217
3118
  ensureVoluteGroup();
4218
3119
  createMindUser(name, homeDir);
4219
3120
  chownMindDir(dest, name);
@@ -4226,7 +3127,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4226
3127
  await initTemplateBranch(dest, composedDir, templateManifest, name, env);
4227
3128
  } catch (err) {
4228
3129
  logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
4229
- rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
3130
+ rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
4230
3131
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
4231
3132
  }
4232
3133
  try {
@@ -4250,7 +3151,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4250
3151
  publishPublicKey(name, publicKeyPem).catch(
4251
3152
  (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
4252
3153
  );
4253
- rmSync2(tempDir, { recursive: true, force: true });
3154
+ rmSync3(tempDir, { recursive: true, force: true });
4254
3155
  return c.json({
4255
3156
  ok: true,
4256
3157
  name,
@@ -4261,24 +3162,24 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4261
3162
  ...skillWarnings.length > 0 && { skillWarnings }
4262
3163
  });
4263
3164
  } catch (err) {
4264
- if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3165
+ if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
4265
3166
  try {
4266
3167
  removeMind(name);
4267
3168
  } catch (cleanupErr) {
4268
3169
  logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
4269
3170
  }
4270
- rmSync2(tempDir, { recursive: true, force: true });
3171
+ rmSync3(tempDir, { recursive: true, force: true });
4271
3172
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
4272
3173
  } finally {
4273
- rmSync2(composedDir, { recursive: true, force: true });
3174
+ rmSync3(composedDir, { recursive: true, force: true });
4274
3175
  }
4275
3176
  }
4276
3177
  async function importHistoryFromArchive(name, tempDir) {
4277
- const historyJsonl = resolve16(tempDir, "history.jsonl");
4278
- if (!existsSync12(historyJsonl)) return;
3178
+ const historyJsonl = resolve11(tempDir, "history.jsonl");
3179
+ if (!existsSync9(historyJsonl)) return;
4279
3180
  try {
4280
3181
  const db = await getDb();
4281
- const lines = readFileSync11(historyJsonl, "utf-8").trim().split("\n");
3182
+ const lines = readFileSync8(historyJsonl, "utf-8").trim().split("\n");
4282
3183
  let imported = 0;
4283
3184
  let failed = 0;
4284
3185
  for (const line of lines) {
@@ -4314,13 +3215,13 @@ async function importHistoryFromArchive(name, tempDir) {
4314
3215
  }
4315
3216
  }
4316
3217
  function importSessionsFromArchive(dest, tempDir) {
4317
- const sessionsDir = resolve16(tempDir, "sessions");
4318
- if (!existsSync12(sessionsDir)) return;
3218
+ const sessionsDir = resolve11(tempDir, "sessions");
3219
+ if (!existsSync9(sessionsDir)) return;
4319
3220
  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));
3221
+ const destSessions = resolve11(dest, ".mind/sessions");
3222
+ mkdirSync5(destSessions, { recursive: true });
3223
+ for (const file of readdirSync4(sessionsDir)) {
3224
+ cpSync(resolve11(sessionsDir, file), resolve11(destSessions, file));
4324
3225
  }
4325
3226
  } catch (err) {
4326
3227
  logger_default.error("Failed to import sessions from archive", logger_default.errorData(err));
@@ -4343,7 +3244,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
4343
3244
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
4344
3245
  ensureVoluteHome();
4345
3246
  const dest = mindDir(name);
4346
- if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3247
+ if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4347
3248
  const templatesRoot = findTemplatesRoot();
4348
3249
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
4349
3250
  try {
@@ -4357,15 +3258,15 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
4357
3258
  writeVoluteConfig(dest, seedConfig);
4358
3259
  }
4359
3260
  if (body.model) {
4360
- const configPath2 = resolve16(dest, "home/.config/config.json");
4361
- const existing = existsSync12(configPath2) ? JSON.parse(readFileSync11(configPath2, "utf-8")) : {};
3261
+ const configPath2 = resolve11(dest, "home/.config/config.json");
3262
+ const existing = existsSync9(configPath2) ? JSON.parse(readFileSync8(configPath2, "utf-8")) : {};
4362
3263
  existing.model = body.model;
4363
- writeFileSync8(configPath2, `${JSON.stringify(existing, null, 2)}
3264
+ writeFileSync6(configPath2, `${JSON.stringify(existing, null, 2)}
4364
3265
  `);
4365
3266
  }
4366
3267
  const mindPrompts = await getMindPromptDefaults();
4367
- writeFileSync8(
4368
- resolve16(dest, "home/.config/prompts.json"),
3268
+ writeFileSync6(
3269
+ resolve11(dest, "home/.config/prompts.json"),
4369
3270
  `${JSON.stringify(mindPrompts, null, 2)}
4370
3271
  `
4371
3272
  );
@@ -4376,7 +3277,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
4376
3277
  } catch (err) {
4377
3278
  logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4378
3279
  }
4379
- const homeDir = resolve16(dest, "home");
3280
+ const homeDir = resolve11(dest, "home");
4380
3281
  ensureVoluteGroup();
4381
3282
  createMindUser(name, homeDir);
4382
3283
  chownMindDir(dest, name);
@@ -4389,7 +3290,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
4389
3290
  await initTemplateBranch(dest, composedDir, manifest, name, env);
4390
3291
  } catch (err) {
4391
3292
  logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
4392
- rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
3293
+ rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
4393
3294
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
4394
3295
  }
4395
3296
  try {
@@ -4404,7 +3305,7 @@ The human who planted you described you as: "${body.description}"
4404
3305
  ` : "";
4405
3306
  const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
4406
3307
  const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
4407
- writeFileSync8(resolve16(dest, "home/SOUL.md"), seedSoul);
3308
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), seedSoul);
4408
3309
  }
4409
3310
  const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
4410
3311
  const skillWarnings = [];
@@ -4419,16 +3320,27 @@ The human who planted you described you as: "${body.description}"
4419
3320
  if (body.stage !== "seed") {
4420
3321
  const customSoul = await getPromptIfCustom("default_soul");
4421
3322
  if (customSoul) {
4422
- writeFileSync8(resolve16(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3323
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
4423
3324
  }
4424
3325
  const customMemory = await getPromptIfCustom("default_memory");
4425
3326
  if (customMemory) {
4426
- writeFileSync8(resolve16(dest, "home/MEMORY.md"), customMemory);
3327
+ writeFileSync6(resolve11(dest, "home/MEMORY.md"), customMemory);
4427
3328
  }
4428
3329
  }
4429
3330
  publishPublicKey(name, publicKeyPem).catch(
4430
3331
  (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
4431
3332
  );
3333
+ fireWebhook({
3334
+ event: "mind_created",
3335
+ mind: name,
3336
+ data: {
3337
+ name,
3338
+ port,
3339
+ stage: body.stage ?? "sprouted",
3340
+ template,
3341
+ description: body.description
3342
+ }
3343
+ });
4432
3344
  return c.json({
4433
3345
  ok: true,
4434
3346
  name,
@@ -4439,14 +3351,14 @@ The human who planted you described you as: "${body.description}"
4439
3351
  ...skillWarnings.length > 0 && { skillWarnings }
4440
3352
  });
4441
3353
  } catch (err) {
4442
- if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3354
+ if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
4443
3355
  try {
4444
3356
  removeMind(name);
4445
3357
  } catch {
4446
3358
  }
4447
3359
  return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
4448
3360
  } finally {
4449
- rmSync2(composedDir, { recursive: true, force: true });
3361
+ rmSync3(composedDir, { recursive: true, force: true });
4450
3362
  }
4451
3363
  }).post("/import", requireAdmin, async (c) => {
4452
3364
  let body;
@@ -4459,13 +3371,13 @@ The human who planted you described you as: "${body.description}"
4459
3371
  return importFromArchive(c, body.archivePath, body.name, body.manifest);
4460
3372
  }
4461
3373
  const wsDir = body.workspacePath;
4462
- if (!wsDir || !existsSync12(resolve16(wsDir, "SOUL.md")) || !existsSync12(resolve16(wsDir, "IDENTITY.md"))) {
3374
+ if (!wsDir || !existsSync9(resolve11(wsDir, "SOUL.md")) || !existsSync9(resolve11(wsDir, "IDENTITY.md"))) {
4463
3375
  return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
4464
3376
  }
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") : "";
3377
+ const soul = readFileSync8(resolve11(wsDir, "SOUL.md"), "utf-8");
3378
+ const identity = readFileSync8(resolve11(wsDir, "IDENTITY.md"), "utf-8");
3379
+ const userPath = resolve11(wsDir, "USER.md");
3380
+ const user = existsSync9(userPath) ? readFileSync8(userPath, "utf-8") : "";
4469
3381
  const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
4470
3382
  const template = body.template ?? "claude";
4471
3383
  const nameErr = validateMindName(name);
@@ -4485,33 +3397,33 @@ ${user.trimEnd()}
4485
3397
  ` : "";
4486
3398
  ensureVoluteHome();
4487
3399
  const dest = mindDir(name);
4488
- if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3400
+ if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4489
3401
  const templatesRoot = findTemplatesRoot();
4490
3402
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
4491
3403
  try {
4492
3404
  copyTemplateToDir(composedDir, dest, name, manifest);
4493
3405
  applyInitFiles(dest);
4494
3406
  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);
3407
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), mergedSoul);
3408
+ const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
3409
+ const hasMemory = existsSync9(wsMemoryPath);
4498
3410
  if (hasMemory) {
4499
- const existingMemory = readFileSync11(wsMemoryPath, "utf-8");
4500
- writeFileSync8(
4501
- resolve16(dest, "home/MEMORY.md"),
3411
+ const existingMemory = readFileSync8(wsMemoryPath, "utf-8");
3412
+ writeFileSync6(
3413
+ resolve11(dest, "home/MEMORY.md"),
4502
3414
  `${existingMemory.trimEnd()}${mergedMemoryExtra}`
4503
3415
  );
4504
3416
  } else if (user) {
4505
- writeFileSync8(resolve16(dest, "home/MEMORY.md"), `${user.trimEnd()}
3417
+ writeFileSync6(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
4506
3418
  `);
4507
3419
  }
4508
- const wsMemoryDir = resolve16(wsDir, "memory");
3420
+ const wsMemoryDir = resolve11(wsDir, "memory");
4509
3421
  let dailyLogCount = 0;
4510
- if (existsSync12(wsMemoryDir)) {
4511
- const destMemoryDir = resolve16(dest, "home/memory");
4512
- const files = readdirSync5(wsMemoryDir).filter((f) => f.endsWith(".md"));
3422
+ if (existsSync9(wsMemoryDir)) {
3423
+ const destMemoryDir = resolve11(dest, "home/memory");
3424
+ const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
4513
3425
  for (const file of files) {
4514
- cpSync(resolve16(wsMemoryDir, file), resolve16(destMemoryDir, file));
3426
+ cpSync(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
4515
3427
  }
4516
3428
  dailyLogCount = files.length;
4517
3429
  }
@@ -4522,7 +3434,7 @@ ${user.trimEnd()}
4522
3434
  } catch (err) {
4523
3435
  logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4524
3436
  }
4525
- const homeDir = resolve16(dest, "home");
3437
+ const homeDir = resolve11(dest, "home");
4526
3438
  ensureVoluteGroup();
4527
3439
  createMindUser(name, homeDir);
4528
3440
  chownMindDir(dest, name);
@@ -4530,20 +3442,20 @@ ${user.trimEnd()}
4530
3442
  if (!hasMemory && dailyLogCount > 0) {
4531
3443
  await consolidateMemory(dest);
4532
3444
  }
4533
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
3445
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
4534
3446
  await gitExec(["init"], { cwd: dest, mindName: name, env });
4535
3447
  await configureGitIdentity(name, { cwd: dest, mindName: name, env });
4536
3448
  await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
4537
3449
  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)) {
3450
+ const sessionFile = body.sessionPath ? resolve11(body.sessionPath) : findOpenClawSession(wsDir);
3451
+ if (sessionFile && existsSync9(sessionFile)) {
4540
3452
  if (template === "pi") {
4541
3453
  importPiSession(sessionFile, dest);
4542
3454
  } else if (template === "claude") {
4543
3455
  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 }));
3456
+ const mindRuntimeDir = resolve11(dest, ".mind");
3457
+ mkdirSync5(mindRuntimeDir, { recursive: true });
3458
+ writeFileSync6(resolve11(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
4547
3459
  }
4548
3460
  }
4549
3461
  importOpenClawConnectors(name, dest);
@@ -4558,14 +3470,14 @@ ${user.trimEnd()}
4558
3470
  );
4559
3471
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
4560
3472
  } catch (err) {
4561
- if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3473
+ if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
4562
3474
  try {
4563
3475
  removeMind(name);
4564
3476
  } catch {
4565
3477
  }
4566
3478
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
4567
3479
  } finally {
4568
- rmSync2(composedDir, { recursive: true, force: true });
3480
+ rmSync3(composedDir, { recursive: true, force: true });
4569
3481
  }
4570
3482
  }).get("/", async (c) => {
4571
3483
  const entries = readRegistry();
@@ -4582,7 +3494,7 @@ ${user.trimEnd()}
4582
3494
  const minds = await Promise.all(
4583
3495
  entries.map(async (entry) => {
4584
3496
  const mindStatus = await getMindStatus(entry.name, entry.port);
4585
- const hasPages = existsSync12(resolve16(mindDir(entry.name), "home", "pages"));
3497
+ const hasPages = existsSync9(resolve11(mindDir(entry.name), "home", "pages"));
4586
3498
  return {
4587
3499
  ...entry,
4588
3500
  ...mindStatus,
@@ -4600,7 +3512,7 @@ ${user.trimEnd()}
4600
3512
  const name = c.req.param("name");
4601
3513
  const entry = findMind(name);
4602
3514
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4603
- if (!existsSync12(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
3515
+ if (!existsSync9(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
4604
3516
  const mindStatus = await getMindStatus(name, entry.port);
4605
3517
  const variants = readVariants(name);
4606
3518
  const manager = getMindManager();
@@ -4615,7 +3527,7 @@ ${user.trimEnd()}
4615
3527
  return { name: v.name, port: v.port, status: variantStatus };
4616
3528
  })
4617
3529
  );
4618
- const hasPages = existsSync12(resolve16(mindDir(name), "home", "pages"));
3530
+ const hasPages = existsSync9(resolve11(mindDir(name), "home", "pages"));
4619
3531
  return c.json({ ...entry, ...mindStatus, variants: variantStatuses, hasPages });
4620
3532
  }).post("/:name/start", requireAdmin, async (c) => {
4621
3533
  const name = c.req.param("name");
@@ -4629,7 +3541,7 @@ ${user.trimEnd()}
4629
3541
  targetPort = variant.port;
4630
3542
  } else {
4631
3543
  const dir = mindDir(baseName);
4632
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3544
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
4633
3545
  }
4634
3546
  if (getMindManager().isRunning(name)) {
4635
3547
  return c.json({ error: "Mind already running" }, 409);
@@ -4652,7 +3564,7 @@ ${user.trimEnd()}
4652
3564
  targetPort = variant.port;
4653
3565
  } else {
4654
3566
  const dir = mindDir(baseName);
4655
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3567
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
4656
3568
  }
4657
3569
  let context;
4658
3570
  const contentType = c.req.header("content-type");
@@ -4679,7 +3591,7 @@ ${user.trimEnd()}
4679
3591
  const variant = findVariant(baseName, mergeVariantName);
4680
3592
  if (variant) {
4681
3593
  const projectRoot = mindDir(baseName);
4682
- if (existsSync12(variant.path)) {
3594
+ if (existsSync9(variant.path)) {
4683
3595
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
4684
3596
  if (status) {
4685
3597
  try {
@@ -4707,20 +3619,7 @@ ${user.trimEnd()}
4707
3619
  }
4708
3620
  }
4709
3621
  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 {
4721
- }
4722
- removeVariant(baseName, mergeVariantName);
4723
- chownMindDir(projectRoot, baseName);
3622
+ await cleanupVariant(baseName, mergeVariantName, projectRoot, variant.path);
4724
3623
  try {
4725
3624
  await npmInstallAsMind(projectRoot, baseName);
4726
3625
  } catch (e) {
@@ -4768,6 +3667,53 @@ ${user.trimEnd()}
4768
3667
  } catch (err) {
4769
3668
  return c.json({ error: err instanceof Error ? err.message : "Failed to stop mind" }, 500);
4770
3669
  }
3670
+ }).get("/:name/sleep", requireAdmin, async (c) => {
3671
+ const name = c.req.param("name");
3672
+ const entry = findMind(name);
3673
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3674
+ const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
3675
+ const sm = getSleepManagerIfReady();
3676
+ if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
3677
+ return c.json(sm.getState(name));
3678
+ }).post("/:name/sleep", requireAdmin, async (c) => {
3679
+ const name = c.req.param("name");
3680
+ const entry = findMind(name);
3681
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3682
+ const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
3683
+ const sm = getSleepManagerIfReady();
3684
+ if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
3685
+ if (sm.isSleeping(name)) return c.json({ error: "Mind is already sleeping" }, 409);
3686
+ const body = await c.req.json().catch(() => ({}));
3687
+ const wakeAt = body.wakeAt;
3688
+ if (wakeAt) {
3689
+ const wakeDate = new Date(wakeAt);
3690
+ if (Number.isNaN(wakeDate.getTime()) || wakeDate <= /* @__PURE__ */ new Date()) {
3691
+ return c.json({ error: "wakeAt must be a valid future ISO date" }, 400);
3692
+ }
3693
+ }
3694
+ sm.initiateSleep(name, wakeAt ? { voluntaryWakeAt: wakeAt } : void 0).catch(
3695
+ (err) => logger_default.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
3696
+ );
3697
+ return c.json({ ok: true });
3698
+ }).post("/:name/wake", requireAdmin, async (c) => {
3699
+ const name = c.req.param("name");
3700
+ const entry = findMind(name);
3701
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3702
+ const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
3703
+ const sm = getSleepManagerIfReady();
3704
+ if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
3705
+ if (!sm.isSleeping(name)) return c.json({ error: "Mind is not sleeping" }, 409);
3706
+ sm.initiateWake(name).catch((err) => logger_default.error(`failed to wake ${name}`, logger_default.errorData(err)));
3707
+ return c.json({ ok: true });
3708
+ }).post("/:name/sleep/messages", requireAdmin, async (c) => {
3709
+ const name = c.req.param("name");
3710
+ const entry = findMind(name);
3711
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3712
+ const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
3713
+ const sm = getSleepManagerIfReady();
3714
+ if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
3715
+ const flushed = await sm.flushQueuedMessages(name);
3716
+ return c.json({ ok: true, flushed });
4771
3717
  }).post("/:name/sprout", requireAdmin, async (c) => {
4772
3718
  const name = c.req.param("name");
4773
3719
  const entry = findMind(name);
@@ -4796,20 +3742,25 @@ ${user.trimEnd()}
4796
3742
  removeMind(name);
4797
3743
  await deleteMindUser2(name);
4798
3744
  const state = stateDir(name);
4799
- if (existsSync12(state)) {
4800
- rmSync2(state, { recursive: true, force: true });
3745
+ if (existsSync9(state)) {
3746
+ rmSync3(state, { recursive: true, force: true });
4801
3747
  }
4802
- if (force && existsSync12(dir)) {
4803
- rmSync2(dir, { recursive: true, force: true });
3748
+ if (force && existsSync9(dir)) {
3749
+ rmSync3(dir, { recursive: true, force: true });
4804
3750
  deleteMindUser(name);
4805
3751
  }
3752
+ fireWebhook({
3753
+ event: "mind_deleted",
3754
+ mind: name,
3755
+ data: { port: entry.port, stage: entry.stage, template: entry.template }
3756
+ });
4806
3757
  return c.json({ ok: true });
4807
3758
  }).post("/:name/upgrade", requireAdmin, async (c) => {
4808
3759
  const mindName = c.req.param("name");
4809
3760
  const entry = findMind(mindName);
4810
3761
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4811
3762
  const dir = mindDir(mindName);
4812
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3763
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
4813
3764
  let body = {};
4814
3765
  try {
4815
3766
  body = await c.req.json();
@@ -4817,9 +3768,32 @@ ${user.trimEnd()}
4817
3768
  }
4818
3769
  const template = body.template ?? entry.template ?? "claude";
4819
3770
  const UPGRADE_VARIANT = "upgrade";
3771
+ if (body.abort) {
3772
+ const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
3773
+ if (!existsSync9(worktreeDir2)) {
3774
+ return c.json({ error: "No upgrade in progress" }, 400);
3775
+ }
3776
+ try {
3777
+ try {
3778
+ const gitDirContent = readFileSync8(resolve11(worktreeDir2, ".git"), "utf-8").trim();
3779
+ const gitDir = gitDirContent.replace("gitdir: ", "");
3780
+ if (existsSync9(resolve11(gitDir, "MERGE_HEAD"))) {
3781
+ await gitExec(["merge", "--abort"], { cwd: worktreeDir2 });
3782
+ }
3783
+ } catch {
3784
+ }
3785
+ await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir2, { stop: true });
3786
+ return c.json({ ok: true });
3787
+ } catch (err) {
3788
+ return c.json(
3789
+ { error: err instanceof Error ? err.message : "Failed to abort upgrade" },
3790
+ 500
3791
+ );
3792
+ }
3793
+ }
4820
3794
  if (body.continue) {
4821
- const worktreeDir2 = resolve16(dir, ".variants", UPGRADE_VARIANT);
4822
- if (!existsSync12(worktreeDir2)) {
3795
+ const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
3796
+ if (!existsSync9(worktreeDir2)) {
4823
3797
  return c.json({ error: "No upgrade in progress" }, 400);
4824
3798
  }
4825
3799
  const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
@@ -4832,7 +3806,10 @@ ${user.trimEnd()}
4832
3806
  await gitExec(["commit", "-m", "merge template update"], { cwd: worktreeDir2 });
4833
3807
  } catch (e) {
4834
3808
  const msg = e instanceof Error ? e.message : String(e);
4835
- if (!msg.includes("nothing to commit")) throw e;
3809
+ const stderr = e?.stderr ?? "";
3810
+ const stdout = e?.stdout ?? "";
3811
+ if (!msg.includes("nothing to commit") && !stderr.includes("nothing to commit") && !stdout.includes("nothing to commit"))
3812
+ throw e;
4836
3813
  }
4837
3814
  chownMindDir(dir, mindName);
4838
3815
  try {
@@ -4853,49 +3830,30 @@ ${user.trimEnd()}
4853
3830
  port: variantPort
4854
3831
  });
4855
3832
  } 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
- }
3833
+ await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir2);
4876
3834
  return c.json(
4877
3835
  { error: err instanceof Error ? err.message : "Failed to continue upgrade" },
4878
3836
  500
4879
3837
  );
4880
3838
  }
4881
3839
  }
4882
- const worktreeDir = resolve16(dir, ".variants", UPGRADE_VARIANT);
4883
- if (existsSync12(worktreeDir)) {
3840
+ const worktreeDir = resolve11(dir, ".variants", UPGRADE_VARIANT);
3841
+ if (existsSync9(worktreeDir)) {
4884
3842
  return c.json(
4885
3843
  { error: "Upgrade variant already exists. Use continue or delete it first." },
4886
3844
  409
4887
3845
  );
4888
3846
  }
4889
- if (!existsSync12(resolve16(dir, ".git"))) {
3847
+ if (!existsSync9(resolve11(dir, ".git"))) {
4890
3848
  try {
4891
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dir, "home") } : void 0;
3849
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dir, "home") } : void 0;
4892
3850
  await gitExec(["init"], { cwd: dir, mindName, env });
4893
3851
  await configureGitIdentity(mindName, { cwd: dir, mindName, env });
4894
3852
  await gitExec(["add", "-A"], { cwd: dir, mindName, env });
4895
3853
  await gitExec(["commit", "-m", "initial commit"], { cwd: dir, mindName, env });
4896
3854
  chownMindDir(dir, mindName);
4897
3855
  } catch (err) {
4898
- rmSync2(resolve16(dir, ".git"), { recursive: true, force: true });
3856
+ rmSync3(resolve11(dir, ".git"), { recursive: true, force: true });
4899
3857
  return c.json(
4900
3858
  {
4901
3859
  error: `Git initialization failed: ${err instanceof Error ? err.message : String(err)}`
@@ -4909,7 +3867,7 @@ ${user.trimEnd()}
4909
3867
  await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
4910
3868
  } catch {
4911
3869
  }
4912
- if (!existsSync12(resolve16(dir, "home", "shared"))) {
3870
+ if (!existsSync9(resolve11(dir, "home", "shared"))) {
4913
3871
  try {
4914
3872
  await addSharedWorktree(mindName, dir);
4915
3873
  } catch (err) {
@@ -4920,9 +3878,9 @@ ${user.trimEnd()}
4920
3878
  }
4921
3879
  }
4922
3880
  await updateTemplateBranch(dir, template, mindName);
4923
- const parentDir = resolve16(dir, ".variants");
4924
- if (!existsSync12(parentDir)) {
4925
- mkdirSync7(parentDir, { recursive: true });
3881
+ const parentDir = resolve11(dir, ".variants");
3882
+ if (!existsSync9(parentDir)) {
3883
+ mkdirSync5(parentDir, { recursive: true });
4926
3884
  }
4927
3885
  await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
4928
3886
  const hasConflicts = await mergeTemplateBranch(worktreeDir);
@@ -4953,26 +3911,7 @@ ${user.trimEnd()}
4953
3911
  port: variantPort
4954
3912
  });
4955
3913
  } 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
- }
3914
+ await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir);
4976
3915
  return c.json(
4977
3916
  { error: err instanceof Error ? err.message : "Failed to complete upgrade" },
4978
3917
  500
@@ -4987,6 +3926,38 @@ ${user.trimEnd()}
4987
3926
  const variant = findVariant(baseName, variantName);
4988
3927
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
4989
3928
  }
3929
+ try {
3930
+ const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
3931
+ const sm = getSleepManagerIfReady();
3932
+ if (sm?.isSleeping(baseName)) {
3933
+ const body2 = await c.req.text();
3934
+ let parsed2 = null;
3935
+ try {
3936
+ parsed2 = JSON.parse(body2);
3937
+ } catch {
3938
+ }
3939
+ if (parsed2) {
3940
+ const payload = {
3941
+ channel: parsed2.channel ?? "unknown",
3942
+ sender: parsed2.sender ?? null,
3943
+ content: parsed2.content,
3944
+ isDM: parsed2.isDM
3945
+ };
3946
+ if (sm.checkWakeTrigger(baseName, payload)) {
3947
+ await sm.queueSleepMessage(baseName, payload);
3948
+ sm.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch(
3949
+ (err) => logger_default.error(`failed to trigger-wake ${baseName}`, logger_default.errorData(err))
3950
+ );
3951
+ return c.json({ ok: true, queued: true, triggerWake: true });
3952
+ }
3953
+ await sm.queueSleepMessage(baseName, payload);
3954
+ return c.json({ ok: true, queued: true });
3955
+ }
3956
+ return c.json({ error: "Invalid JSON" }, 400);
3957
+ }
3958
+ } catch (err) {
3959
+ logger_default.error(`failed to check sleep state for ${baseName}`, logger_default.errorData(err));
3960
+ }
4990
3961
  if (!getMindManager().isRunning(name)) {
4991
3962
  return c.json({ error: "Mind is not running" }, 409);
4992
3963
  }
@@ -4998,21 +3969,10 @@ ${user.trimEnd()}
4998
3969
  logger_default.error(`failed to parse message body for ${baseName}`, logger_default.errorData(err));
4999
3970
  }
5000
3971
  const channel = parsed?.channel ?? "unknown";
5001
- const db = await getDb();
5002
3972
  if (parsed) {
5003
- try {
5004
- const sender2 = parsed.sender ?? null;
5005
- const content = extractTextContent(parsed.content);
5006
- await db.insert(mindHistory).values({
5007
- mind: baseName,
5008
- type: "inbound",
5009
- channel,
5010
- sender: sender2,
5011
- content
5012
- });
5013
- } catch (err) {
5014
- logger_default.error(`failed to persist inbound message for ${baseName}`, logger_default.errorData(err));
5015
- }
3973
+ const sender2 = parsed.sender ?? null;
3974
+ const content = extractTextContent(parsed.content);
3975
+ await recordInbound(baseName, channel, sender2, content);
5016
3976
  }
5017
3977
  const budget = getTokenBudget();
5018
3978
  const budgetStatus = budget.checkBudget(baseName);
@@ -5064,6 +4024,7 @@ ${user.trimEnd()}
5064
4024
  const seedEntry = findMind(baseName);
5065
4025
  if (seedEntry?.stage === "seed") {
5066
4026
  try {
4027
+ const db = await getDb();
5067
4028
  const countResult = await db.select({ count: sql2`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
5068
4029
  const msgCount = countResult[0]?.count ?? 0;
5069
4030
  if (msgCount >= 10 && msgCount % 10 === 0) {
@@ -5109,13 +4070,13 @@ ${user.trimEnd()}
5109
4070
  const entry = findMind(name);
5110
4071
  if (!entry) return c.json({ error: "Mind not found" }, 404);
5111
4072
  const dir = mindDir(name);
5112
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
4073
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
5113
4074
  let config = readVoluteConfig(dir);
5114
4075
  if (!config && entry.template === "pi") {
5115
- const piConfigPath = resolve16(dir, "home/.config/config.json");
5116
- if (existsSync12(piConfigPath)) {
4076
+ const piConfigPath = resolve11(dir, "home/.config/config.json");
4077
+ if (existsSync9(piConfigPath)) {
5117
4078
  try {
5118
- config = JSON.parse(readFileSync11(piConfigPath, "utf-8"));
4079
+ config = JSON.parse(readFileSync8(piConfigPath, "utf-8"));
5119
4080
  } catch {
5120
4081
  }
5121
4082
  }
@@ -5152,7 +4113,7 @@ ${user.trimEnd()}
5152
4113
  const entry = findMind(name);
5153
4114
  if (!entry) return c.json({ error: "Mind not found" }, 404);
5154
4115
  const dir = mindDir(name);
5155
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
4116
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
5156
4117
  const body = c.req.valid("json");
5157
4118
  const existing = readVoluteConfig(dir) ?? {};
5158
4119
  if (body.model !== void 0) existing.model = body.model;
@@ -5215,7 +4176,7 @@ ${user.trimEnd()}
5215
4176
  } catch (err) {
5216
4177
  logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
5217
4178
  }
5218
- publish3(baseName, {
4179
+ publish(baseName, {
5219
4180
  mind: baseName,
5220
4181
  type: body.type,
5221
4182
  session: body.session,
@@ -5265,14 +4226,29 @@ ${user.trimEnd()}
5265
4226
 
5266
4227
  `));
5267
4228
  };
5268
- const unsubscribe = subscribe3(baseName, (event) => {
4229
+ let unsubscribe;
4230
+ const pingInterval = setInterval(() => {
4231
+ try {
4232
+ controller.enqueue(encoder.encode(": ping\n\n"));
4233
+ } catch {
4234
+ clearInterval(pingInterval);
4235
+ unsubscribe?.();
4236
+ }
4237
+ }, 15e3);
4238
+ unsubscribe = subscribe2(baseName, (event) => {
5269
4239
  if (typeFilter && !typeFilter.includes(event.type)) return;
5270
4240
  if (sessionFilter && event.session !== sessionFilter) return;
5271
4241
  if (channelFilter && event.channel !== channelFilter) return;
5272
- send5(JSON.stringify(event));
4242
+ try {
4243
+ send5(JSON.stringify(event));
4244
+ } catch {
4245
+ clearInterval(pingInterval);
4246
+ unsubscribe?.();
4247
+ }
5273
4248
  });
5274
4249
  c.req.raw.signal.addEventListener("abort", () => {
5275
- unsubscribe();
4250
+ clearInterval(pingInterval);
4251
+ unsubscribe?.();
5276
4252
  try {
5277
4253
  controller.close();
5278
4254
  } catch {
@@ -5360,7 +4336,7 @@ var minds_default = app11;
5360
4336
 
5361
4337
  // src/web/api/pages.ts
5362
4338
  import { readFile as readFile2, stat as stat2 } from "fs/promises";
5363
- import { extname as extname2, resolve as resolve17 } from "path";
4339
+ import { extname as extname2, resolve as resolve12 } from "path";
5364
4340
  import { Hono as Hono12 } from "hono";
5365
4341
  var MIME_TYPES = {
5366
4342
  ".html": "text/html",
@@ -5382,17 +4358,17 @@ var app12 = new Hono12().get("/:name/*", async (c) => {
5382
4358
  const name = c.req.param("name");
5383
4359
  let pagesRoot;
5384
4360
  if (name === "_system") {
5385
- pagesRoot = resolve17(voluteHome(), "shared", "pages");
4361
+ pagesRoot = resolve12(voluteHome(), "shared", "pages");
5386
4362
  } else {
5387
4363
  if (!findMind(name)) return c.text("Not found", 404);
5388
- pagesRoot = resolve17(mindDir(name), "home", "pages");
4364
+ pagesRoot = resolve12(mindDir(name), "home", "pages");
5389
4365
  }
5390
4366
  const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
5391
- const requestedPath = resolve17(pagesRoot, wildcard.slice(1));
4367
+ const requestedPath = resolve12(pagesRoot, wildcard.slice(1));
5392
4368
  if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
5393
4369
  let fileStat = await stat2(requestedPath).catch(() => null);
5394
4370
  if (fileStat?.isDirectory()) {
5395
- const indexPath = resolve17(requestedPath, "index.html");
4371
+ const indexPath = resolve12(requestedPath, "index.html");
5396
4372
  fileStat = await stat2(indexPath).catch(() => null);
5397
4373
  if (fileStat?.isFile()) {
5398
4374
  const body = await readFile2(indexPath);
@@ -5462,9 +4438,9 @@ var app13 = new Hono13().get("/", async (c) => {
5462
4438
  var prompts_default = app13;
5463
4439
 
5464
4440
  // src/web/api/schedules.ts
5465
- import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
4441
+ import { CronExpressionParser } from "cron-parser";
5466
4442
  import { Hono as Hono14 } from "hono";
5467
- var slog2 = logger_default.child("schedules");
4443
+ var slog = logger_default.child("schedules");
5468
4444
  function readSchedules(name) {
5469
4445
  return readVoluteConfig(mindDir(name))?.schedules ?? [];
5470
4446
  }
@@ -5474,6 +4450,11 @@ function writeSchedules(name, schedules) {
5474
4450
  config.schedules = schedules.length > 0 ? schedules : void 0;
5475
4451
  writeVoluteConfig(dir, config);
5476
4452
  getScheduler().loadSchedules(name);
4453
+ fireWebhook({
4454
+ event: "schedule_changed",
4455
+ mind: name,
4456
+ data: { schedules }
4457
+ });
5477
4458
  }
5478
4459
  var app14 = new Hono14().get("/:name/schedules", (c) => {
5479
4460
  const name = c.req.param("name");
@@ -5496,7 +4477,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
5496
4477
  return c.json({ error: "message and script are mutually exclusive" }, 400);
5497
4478
  }
5498
4479
  try {
5499
- CronExpressionParser2.parse(body.cron);
4480
+ CronExpressionParser.parse(body.cron);
5500
4481
  } catch {
5501
4482
  return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
5502
4483
  }
@@ -5524,7 +4505,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
5524
4505
  }
5525
4506
  if (body.cron !== void 0) {
5526
4507
  try {
5527
- CronExpressionParser2.parse(body.cron);
4508
+ CronExpressionParser.parse(body.cron);
5528
4509
  } catch {
5529
4510
  return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
5530
4511
  }
@@ -5574,7 +4555,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
5574
4555
  }
5575
4556
  return c.json({ ok: true });
5576
4557
  } catch (err) {
5577
- slog2.warn(`webhook delivery failed for ${name}`, logger_default.errorData(err));
4558
+ slog.warn(`webhook delivery failed for ${name}`, logger_default.errorData(err));
5578
4559
  return c.json({ error: "Failed to reach mind" }, 502);
5579
4560
  }
5580
4561
  });
@@ -5634,9 +4615,9 @@ var app15 = new Hono15().post("/:name/shared/merge", requireAdmin, async (c) =>
5634
4615
  var shared_default = app15;
5635
4616
 
5636
4617
  // src/web/api/skills.ts
5637
- import { existsSync as existsSync13, mkdtempSync, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
4618
+ import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4 } from "fs";
5638
4619
  import { tmpdir } from "os";
5639
- import { join as join3, resolve as resolve18 } from "path";
4620
+ import { join as join2, resolve as resolve13 } from "path";
5640
4621
  import AdmZip from "adm-zip";
5641
4622
  import { Hono as Hono16 } from "hono";
5642
4623
  var app16 = new Hono16().get("/", async (c) => {
@@ -5646,7 +4627,7 @@ var app16 = new Hono16().get("/", async (c) => {
5646
4627
  const id = c.req.param("id");
5647
4628
  const skill = await getSharedSkill(id);
5648
4629
  if (!skill) return c.json({ error: "Skill not found" }, 404);
5649
- const dir = join3(sharedSkillsDir(), id);
4630
+ const dir = join2(sharedSkillsDir(), id);
5650
4631
  const files = listFilesRecursive(dir);
5651
4632
  return c.json({ ...skill, files });
5652
4633
  }).post("/upload", requireAdmin, async (c) => {
@@ -5658,25 +4639,25 @@ var app16 = new Hono16().get("/", async (c) => {
5658
4639
  if (!file.name.endsWith(".zip")) {
5659
4640
  return c.json({ error: "Only .zip files are accepted" }, 400);
5660
4641
  }
5661
- const buffer = Buffer.from(await file.arrayBuffer());
5662
- const tmpDir = mkdtempSync(join3(tmpdir(), "volute-skill-upload-"));
4642
+ const buffer2 = Buffer.from(await file.arrayBuffer());
4643
+ const tmpDir = mkdtempSync(join2(tmpdir(), "volute-skill-upload-"));
5663
4644
  try {
5664
- const zip = new AdmZip(buffer);
4645
+ const zip = new AdmZip(buffer2);
5665
4646
  for (const entry of zip.getEntries()) {
5666
- const target = resolve18(tmpDir, entry.entryName);
4647
+ const target = resolve13(tmpDir, entry.entryName);
5667
4648
  if (!target.startsWith(tmpDir)) {
5668
4649
  return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
5669
4650
  }
5670
4651
  }
5671
4652
  zip.extractAllTo(tmpDir, true);
5672
4653
  let skillDir = null;
5673
- if (existsSync13(join3(tmpDir, "SKILL.md"))) {
4654
+ if (existsSync10(join2(tmpDir, "SKILL.md"))) {
5674
4655
  skillDir = tmpDir;
5675
4656
  } else {
5676
- const entries = readdirSync6(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4657
+ const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
5677
4658
  for (const entry of entries) {
5678
- if (existsSync13(join3(tmpDir, entry.name, "SKILL.md"))) {
5679
- skillDir = join3(tmpDir, entry.name);
4659
+ if (existsSync10(join2(tmpDir, entry.name, "SKILL.md"))) {
4660
+ skillDir = join2(tmpDir, entry.name);
5680
4661
  break;
5681
4662
  }
5682
4663
  }
@@ -5692,7 +4673,7 @@ var app16 = new Hono16().get("/", async (c) => {
5692
4673
  }
5693
4674
  throw e;
5694
4675
  } finally {
5695
- rmSync3(tmpDir, { recursive: true, force: true });
4676
+ rmSync4(tmpDir, { recursive: true, force: true });
5696
4677
  }
5697
4678
  }).delete("/:id", requireAdmin, async (c) => {
5698
4679
  const id = c.req.param("id");
@@ -5717,93 +4698,487 @@ var app17 = new Hono17().post("/restart", requireAdmin, (c) => {
5717
4698
  return c.json({ ok: true });
5718
4699
  }).get("/logs", async (c) => {
5719
4700
  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(() => {
4701
+ if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
4702
+ return streamSSE3(c, async (stream) => {
4703
+ for (const entry of logBuffer.getEntries()) {
4704
+ await stream.writeSSE({ data: JSON.stringify(entry) });
4705
+ }
4706
+ const unsubscribe = logBuffer.subscribe((entry) => {
4707
+ stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
4708
+ });
4709
+ });
4710
+ await new Promise((resolve18) => {
4711
+ stream.onAbort(() => {
4712
+ unsubscribe();
4713
+ resolve18();
4714
+ });
4715
+ });
4716
+ });
4717
+ }).get("/info", (c) => {
4718
+ const config = readSystemsConfig();
4719
+ return c.json({ system: config?.system ?? null });
4720
+ });
4721
+ var system_default = app17;
4722
+
4723
+ // src/web/api/typing.ts
4724
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
4725
+ import { Hono as Hono18 } from "hono";
4726
+ import { z as z5 } from "zod";
4727
+ var typingSchema = z5.object({
4728
+ channel: z5.string().min(1),
4729
+ sender: z5.string().min(1),
4730
+ active: z5.boolean()
4731
+ });
4732
+ var app18 = new Hono18().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
4733
+ const { channel, sender, active } = c.req.valid("json");
4734
+ const map = getTypingMap();
4735
+ if (active) {
4736
+ map.set(channel, sender);
4737
+ } else {
4738
+ map.delete(channel, sender);
4739
+ }
4740
+ const volutePrefix = "volute:";
4741
+ if (channel.startsWith(volutePrefix)) {
4742
+ const conversationId = channel.slice(volutePrefix.length);
4743
+ publish2(conversationId, { type: "typing", senders: map.get(channel) });
4744
+ }
4745
+ return c.json({ ok: true });
4746
+ }).get("/:name/typing", (c) => {
4747
+ const channel = c.req.query("channel");
4748
+ if (!channel) {
4749
+ return c.json({ error: "channel query param is required" }, 400);
4750
+ }
4751
+ const map = getTypingMap();
4752
+ return c.json({ typing: map.get(channel) });
4753
+ });
4754
+ var typing_default = app18;
4755
+
4756
+ // src/web/api/update.ts
4757
+ import { spawn as spawn2 } from "child_process";
4758
+ import { Hono as Hono19 } from "hono";
4759
+ var bin;
4760
+ var app19 = new Hono19().get("/update", async (c) => {
4761
+ const result = await checkForUpdate();
4762
+ return c.json(result);
4763
+ }).post("/update", requireAdmin, async (c) => {
4764
+ bin ??= resolveVoluteBin();
4765
+ const child = spawn2(bin, ["update"], {
4766
+ stdio: "ignore",
4767
+ detached: true
4768
+ });
4769
+ child.on("error", (err) => {
4770
+ logger_default.error("Update process error", { error: err.message });
4771
+ });
4772
+ child.unref();
4773
+ return c.json({ ok: true, message: "Updating..." });
4774
+ });
4775
+ var update_default = app19;
4776
+
4777
+ // src/web/api/v1/chat.ts
4778
+ import { zValidator as zValidator6 } from "@hono/zod-validator";
4779
+ import { Hono as Hono20 } from "hono";
4780
+ import { streamSSE as streamSSE4 } from "hono/streaming";
4781
+ import { z as z6 } from "zod";
4782
+ async function fanOutToMinds(opts) {
4783
+ const participants = await getParticipants(opts.conversationId);
4784
+ const mindParticipants = participants.filter((p) => p.userType === "mind");
4785
+ const participantNames = participants.map((p) => p.username);
4786
+ const isDM = opts.isDM ?? participants.length === 2;
4787
+ const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
4788
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-KMY4GA2J.js");
4789
+ const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
4790
+ const manager = getMindManager2();
4791
+ const sm = getSleepManagerIfReady();
4792
+ const targetMinds = mindParticipants.map((ap) => {
4793
+ const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
4794
+ if (manager.isRunning(key) || sm?.isSleeping(ap.username)) return ap.username;
4795
+ return null;
4796
+ }).filter((n) => n !== null && n !== opts.senderName);
4797
+ function slugForMind(mindUsername) {
4798
+ return buildVoluteSlug({
4799
+ participants,
4800
+ mindUsername,
4801
+ convTitle: opts.convTitle,
4802
+ conversationId: opts.conversationId,
4803
+ ...opts.slugExtra
4804
+ });
4805
+ }
4806
+ const channelEntry = {
4807
+ platformId: opts.conversationId,
4808
+ platform: "volute",
4809
+ name: opts.convTitle ?? void 0,
4810
+ type: channelEntryType
4811
+ };
4812
+ for (const ap of mindParticipants) {
4813
+ try {
4814
+ writeChannelEntry(ap.username, slugForMind(ap.username), channelEntry);
4815
+ } catch (err) {
4816
+ logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
4817
+ }
4818
+ }
4819
+ for (const mindName of targetMinds) {
4820
+ const target = opts.targetName ? opts.targetName(mindName) : mindName;
4821
+ const channel = slugForMind(mindName);
4822
+ const typingMap = getTypingMap();
4823
+ const currentlyTyping = typingMap.get(channel).filter((name) => participantNames.includes(name));
4824
+ deliverMessage(target, {
4825
+ content: opts.contentBlocks,
4826
+ channel,
4827
+ conversationId: opts.conversationId,
4828
+ sender: opts.senderName,
4829
+ participants: participantNames,
4830
+ participantCount: participants.length,
4831
+ isDM,
4832
+ ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
4833
+ }).catch((err) => {
4834
+ logger_default.warn("[v1-chat] delivery failed", logger_default.errorData(err));
4835
+ });
4836
+ }
4837
+ }
4838
+ var mindChatSchema = z6.object({
4839
+ message: z6.string().optional(),
4840
+ conversationId: z6.string().optional(),
4841
+ sender: z6.string().optional(),
4842
+ images: z6.array(z6.object({ media_type: z6.string(), data: z6.string() })).optional()
4843
+ });
4844
+ var unifiedChatSchema = z6.object({
4845
+ message: z6.string().optional(),
4846
+ conversationId: z6.string(),
4847
+ images: z6.array(z6.object({ media_type: z6.string(), data: z6.string() })).optional()
4848
+ });
4849
+ var app20 = new Hono20().use("*", authMiddleware).post("/minds/:name/chat", zValidator6("json", mindChatSchema), async (c) => {
4850
+ const name = c.req.param("name");
4851
+ const [baseName] = name.split("@", 2);
4852
+ const entry = findMind(baseName);
4853
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
4854
+ const body = c.req.valid("json");
4855
+ if (!body.message && (!body.images || body.images.length === 0)) {
4856
+ return c.json({ error: "message or images required" }, 400);
4857
+ }
4858
+ const user = c.get("user");
4859
+ const mindUser = await getOrCreateMindUser(baseName);
4860
+ const senderName = user.id === 0 && body.sender ? body.sender : user.username;
4861
+ let conversationId = body.conversationId;
4862
+ if (conversationId) {
4863
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
4864
+ return c.json({ error: "Conversation not found" }, 404);
4865
+ }
4866
+ } else {
4867
+ const participantIds = [];
4868
+ if (user.id !== 0) {
4869
+ participantIds.push(user.id);
4870
+ } else if (body.sender) {
4871
+ const senderMind = findMind(body.sender);
4872
+ if (senderMind) {
4873
+ const senderMindUser = await getOrCreateMindUser(body.sender);
4874
+ participantIds.push(senderMindUser.id);
4875
+ }
4876
+ }
4877
+ participantIds.push(mindUser.id);
4878
+ if (participantIds.length === 2) {
4879
+ const existing = await findDMConversation(baseName, participantIds);
4880
+ if (existing) conversationId = existing;
4881
+ }
4882
+ if (!conversationId) {
4883
+ const participantNames = /* @__PURE__ */ new Set([senderName, baseName]);
4884
+ const title = [...participantNames].join(", ");
4885
+ const conv2 = await createConversation(baseName, "volute", {
4886
+ userId: user.id !== 0 ? user.id : void 0,
4887
+ title,
4888
+ participantIds
4889
+ });
4890
+ conversationId = conv2.id;
4891
+ }
4892
+ }
4893
+ const conv = await getConversation(conversationId);
4894
+ const convTitle = conv?.title ?? null;
4895
+ const contentBlocks = [];
4896
+ if (body.message) contentBlocks.push({ type: "text", text: body.message });
4897
+ if (body.images) {
4898
+ for (const img of body.images) {
4899
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
4900
+ }
4901
+ }
4902
+ await addMessage(conversationId, "user", senderName, contentBlocks);
4903
+ await fanOutToMinds({
4904
+ conversationId,
4905
+ contentBlocks,
4906
+ senderName,
4907
+ convTitle,
4908
+ targetName: (username) => username === baseName ? name : username
4909
+ });
4910
+ return c.json({ ok: true, conversationId });
4911
+ }).get("/minds/:name/conversations/:id/events", async (c) => {
4912
+ const conversationId = c.req.param("id");
4913
+ const user = c.get("user");
4914
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
4915
+ return c.json({ error: "Conversation not found" }, 404);
4916
+ }
4917
+ return streamSSE4(c, async (stream) => {
4918
+ const unsubscribe = subscribe3(conversationId, (event) => {
4919
+ stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
4920
+ if (!stream.aborted) logger_default.error("[v1-chat] SSE write error:", logger_default.errorData(err));
5727
4921
  });
5728
4922
  });
5729
- await new Promise((resolve23) => {
4923
+ const keepAlive = setInterval(() => {
4924
+ stream.writeSSE({ data: "" }).catch((err) => {
4925
+ if (!stream.aborted) logger_default.error("[v1-chat] SSE ping error:", logger_default.errorData(err));
4926
+ });
4927
+ }, 15e3);
4928
+ await new Promise((resolve18) => {
5730
4929
  stream.onAbort(() => {
5731
4930
  unsubscribe();
5732
- resolve23();
4931
+ clearInterval(keepAlive);
4932
+ resolve18();
5733
4933
  });
5734
4934
  });
5735
4935
  });
5736
- }).get("/info", (c) => {
5737
- const config = readSystemsConfig();
5738
- return c.json({ system: config?.system ?? null });
4936
+ }).post("/chat", zValidator6("json", unifiedChatSchema), async (c) => {
4937
+ const user = c.get("user");
4938
+ const body = c.req.valid("json");
4939
+ if (!body.message && (!body.images || body.images.length === 0)) {
4940
+ return c.json({ error: "message or images required" }, 400);
4941
+ }
4942
+ const conv = await getConversation(body.conversationId);
4943
+ if (!conv) return c.json({ error: "Conversation not found" }, 404);
4944
+ if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
4945
+ return c.json({ error: "Conversation not found" }, 404);
4946
+ }
4947
+ const senderName = user.username;
4948
+ const contentBlocks = [];
4949
+ if (body.message) contentBlocks.push({ type: "text", text: body.message });
4950
+ if (body.images) {
4951
+ for (const img of body.images) {
4952
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
4953
+ }
4954
+ }
4955
+ await addMessage(body.conversationId, "user", senderName, contentBlocks);
4956
+ const isDM = conv.type === "dm";
4957
+ await fanOutToMinds({
4958
+ conversationId: body.conversationId,
4959
+ contentBlocks,
4960
+ senderName,
4961
+ convTitle: conv.title,
4962
+ isDM,
4963
+ channelEntryType: conv.type === "channel" ? "group" : isDM ? "dm" : "group",
4964
+ slugExtra: { convType: conv.type, convName: conv.name }
4965
+ });
4966
+ return c.json({ ok: true, conversationId: body.conversationId });
5739
4967
  });
5740
- var system_default = app17;
4968
+ var chat_default = app20;
5741
4969
 
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()
4970
+ // src/web/api/v1/conversations.ts
4971
+ import { zValidator as zValidator7 } from "@hono/zod-validator";
4972
+ import { Hono as Hono21 } from "hono";
4973
+ import { z as z7 } from "zod";
4974
+ var createSchema = z7.object({
4975
+ title: z7.string().optional(),
4976
+ participantNames: z7.array(z7.string()).min(1)
5750
4977
  });
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);
4978
+ var app21 = new Hono21().use("*", authMiddleware).get("/", async (c) => {
4979
+ const user = c.get("user");
4980
+ const convs = await listConversationsWithParticipants(user.id);
4981
+ return c.json(convs);
4982
+ }).get("/:id/messages", async (c) => {
4983
+ const id = c.req.param("id");
4984
+ const user = c.get("user");
4985
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
4986
+ return c.json({ error: "Conversation not found" }, 404);
5758
4987
  }
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) });
4988
+ const beforeStr = c.req.query("before");
4989
+ const limitStr = c.req.query("limit");
4990
+ if (!beforeStr && !limitStr) {
4991
+ const msgs = await getMessages(id);
4992
+ return c.json({ items: msgs, hasMore: false });
5763
4993
  }
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);
4994
+ const before = beforeStr ? parseInt(beforeStr, 10) : void 0;
4995
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
4996
+ if (before !== void 0 && isNaN(before) || limit !== void 0 && isNaN(limit)) {
4997
+ return c.json({ error: "Invalid cursor params: before and limit must be integers" }, 400);
5769
4998
  }
5770
- const map = getTypingMap();
5771
- return c.json({ typing: map.get(channel) });
4999
+ const result = await getMessagesPaginated(id, { before, limit });
5000
+ return c.json({ items: result.messages, hasMore: result.hasMore });
5001
+ }).get("/:id/participants", async (c) => {
5002
+ const id = c.req.param("id");
5003
+ const user = c.get("user");
5004
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
5005
+ return c.json({ error: "Conversation not found" }, 404);
5006
+ }
5007
+ const participants = await getParticipants(id);
5008
+ return c.json(participants);
5009
+ }).post("/", zValidator7("json", createSchema), async (c) => {
5010
+ const user = c.get("user");
5011
+ const body = c.req.valid("json");
5012
+ const participantIds = /* @__PURE__ */ new Set();
5013
+ if (user.id !== 0) participantIds.add(user.id);
5014
+ let firstMindName;
5015
+ for (const name of body.participantNames) {
5016
+ const existing = await getUserByUsername(name);
5017
+ if (existing) {
5018
+ participantIds.add(existing.id);
5019
+ if (!firstMindName && existing.user_type === "mind") firstMindName = name;
5020
+ continue;
5021
+ }
5022
+ if (findMind(name)) {
5023
+ const au = await getOrCreateMindUser(name);
5024
+ participantIds.add(au.id);
5025
+ if (!firstMindName) firstMindName = name;
5026
+ continue;
5027
+ }
5028
+ return c.json({ error: `User not found: ${name}` }, 400);
5029
+ }
5030
+ if (!firstMindName) {
5031
+ return c.json({ error: "At least one mind participant is required" }, 400);
5032
+ }
5033
+ const conv = await createConversation(firstMindName, "volute", {
5034
+ userId: user.id !== 0 ? user.id : void 0,
5035
+ title: body.title,
5036
+ participantIds: [...participantIds]
5037
+ });
5038
+ return c.json(conv, 201);
5039
+ }).delete("/:id", async (c) => {
5040
+ const id = c.req.param("id");
5041
+ const user = c.get("user");
5042
+ const deleted = await deleteConversationForUser(id, user.id);
5043
+ if (!deleted) return c.json({ error: "Conversation not found" }, 404);
5044
+ return c.json({ ok: true });
5772
5045
  });
5773
- var typing_default = app18;
5046
+ var conversations_default = app21;
5774
5047
 
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 });
5048
+ // src/web/api/v1/events.ts
5049
+ import { desc as desc4 } from "drizzle-orm";
5050
+ import { Hono as Hono22 } from "hono";
5051
+ import { streamSSE as streamSSE5 } from "hono/streaming";
5052
+
5053
+ // src/lib/events/event-sequencer.ts
5054
+ var BUFFER_SIZE = 1e3;
5055
+ var MAX_AGE_MS = 5 * 60 * 1e3;
5056
+ var nextId = 1;
5057
+ var buffer = [];
5058
+ function bufferEvent(data) {
5059
+ const id = nextId++;
5060
+ buffer.push({ id, data, timestamp: Date.now() });
5061
+ while (buffer.length > BUFFER_SIZE) {
5062
+ buffer.shift();
5063
+ }
5064
+ return id;
5065
+ }
5066
+ function getEventsSince(sinceId) {
5067
+ const now = Date.now();
5068
+ const startIdx = buffer.findIndex((e) => e.id > sinceId);
5069
+ if (startIdx === -1) return [];
5070
+ return buffer.slice(startIdx).filter((e) => now - e.timestamp < MAX_AGE_MS);
5071
+ }
5072
+
5073
+ // src/web/api/v1/events.ts
5074
+ var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
5075
+ const user = c.get("user");
5076
+ const since = c.req.query("since");
5077
+ const sinceId = since ? Number(since) : 0;
5078
+ return streamSSE5(c, async (stream) => {
5079
+ const cleanups = [];
5080
+ try {
5081
+ if (sinceId > 0) {
5082
+ const missed = getEventsSince(sinceId);
5083
+ for (const event of missed) {
5084
+ await stream.writeSSE({
5085
+ id: String(event.id),
5086
+ data: JSON.stringify(event.data)
5087
+ });
5088
+ }
5089
+ }
5090
+ let recentActivity = [];
5091
+ try {
5092
+ const db = await getDb();
5093
+ recentActivity = await db.select().from(activity).orderBy(desc4(activity.created_at)).limit(50);
5094
+ recentActivity = recentActivity.map((row) => ({
5095
+ ...row,
5096
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
5097
+ }));
5098
+ } catch (err) {
5099
+ logger_default.error("[v1-events] failed to fetch recent activity", logger_default.errorData(err));
5100
+ }
5101
+ let conversations2 = [];
5102
+ try {
5103
+ conversations2 = await listConversationsWithParticipants(user.id);
5104
+ } catch (err) {
5105
+ logger_default.error("[v1-events] failed to fetch conversations", logger_default.errorData(err));
5106
+ }
5107
+ const sites = getCachedSites();
5108
+ const recentPages = getCachedRecentPages();
5109
+ const snapshotData = {
5110
+ event: "snapshot",
5111
+ activity: recentActivity,
5112
+ conversations: conversations2,
5113
+ sites,
5114
+ recentPages,
5115
+ activeMinds: getActiveMinds()
5116
+ };
5117
+ const snapshotId = bufferEvent(snapshotData);
5118
+ await stream.writeSSE({
5119
+ id: String(snapshotId),
5120
+ data: JSON.stringify(snapshotData)
5121
+ });
5122
+ const unsubActivity = subscribe((event) => {
5123
+ const data = {
5124
+ event: "activity",
5125
+ ...event,
5126
+ metadata: event.metadata ?? null
5127
+ };
5128
+ const eventId = bufferEvent(data);
5129
+ stream.writeSSE({
5130
+ id: String(eventId),
5131
+ data: JSON.stringify(data)
5132
+ }).catch((err) => {
5133
+ if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
5134
+ });
5135
+ });
5136
+ cleanups.push(unsubActivity);
5137
+ for (const conv of conversations2) {
5138
+ const unsubConv = subscribe3(conv.id, (event) => {
5139
+ const data = { event: "conversation", conversationId: conv.id, ...event };
5140
+ const eventId = bufferEvent(data);
5141
+ stream.writeSSE({
5142
+ id: String(eventId),
5143
+ data: JSON.stringify(data)
5144
+ }).catch((err) => {
5145
+ if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
5146
+ });
5147
+ });
5148
+ cleanups.push(unsubConv);
5149
+ }
5150
+ const keepAlive = setInterval(() => {
5151
+ stream.writeSSE({ data: "" }).catch((err) => {
5152
+ if (!stream.aborted) logger_default.error("[v1-events] ping error:", logger_default.errorData(err));
5153
+ });
5154
+ }, 15e3);
5155
+ cleanups.push(() => clearInterval(keepAlive));
5156
+ await new Promise((resolve18) => {
5157
+ stream.onAbort(() => resolve18());
5158
+ });
5159
+ } finally {
5160
+ for (const cleanup of cleanups) {
5161
+ try {
5162
+ cleanup();
5163
+ } catch {
5164
+ }
5165
+ }
5166
+ }
5790
5167
  });
5791
- child.unref();
5792
- return c.json({ ok: true, message: "Updating..." });
5793
5168
  });
5794
- var update_default = app19;
5169
+ var events_default = app22;
5795
5170
 
5796
5171
  // 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";
5172
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
5173
+ import { resolve as resolve15 } from "path";
5174
+ import { Hono as Hono23 } from "hono";
5800
5175
 
5801
5176
  // 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";
5177
+ import { spawn as spawn3 } from "child_process";
5178
+ import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync9 } from "fs";
5179
+ import { resolve as resolve14 } from "path";
5805
5180
  function tsxBin(cwd) {
5806
- return resolve19(cwd, "node_modules", ".bin", "tsx");
5181
+ return resolve14(cwd, "node_modules", ".bin", "tsx");
5807
5182
  }
5808
5183
  function spawnServer(cwd, port, options) {
5809
5184
  if (options?.detached) {
@@ -5812,37 +5187,37 @@ function spawnServer(cwd, port, options) {
5812
5187
  return spawnAttached(cwd, port);
5813
5188
  }
5814
5189
  function spawnAttached(cwd, port) {
5815
- const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
5190
+ const child = spawn3(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
5816
5191
  cwd,
5817
5192
  stdio: ["ignore", "pipe", "pipe"]
5818
5193
  });
5819
- return new Promise((resolve23) => {
5820
- const timeout = setTimeout(() => resolve23(null), 3e4);
5194
+ return new Promise((resolve18) => {
5195
+ const timeout = setTimeout(() => resolve18(null), 3e4);
5821
5196
  function checkOutput(data) {
5822
5197
  const match = data.toString().match(/listening on :(\d+)/);
5823
5198
  if (match) {
5824
5199
  clearTimeout(timeout);
5825
- resolve23({ child, actualPort: parseInt(match[1], 10) });
5200
+ resolve18({ child, actualPort: parseInt(match[1], 10) });
5826
5201
  }
5827
5202
  }
5828
5203
  child.stdout?.on("data", checkOutput);
5829
5204
  child.stderr?.on("data", checkOutput);
5830
5205
  child.on("error", () => {
5831
5206
  clearTimeout(timeout);
5832
- resolve23(null);
5207
+ resolve18(null);
5833
5208
  });
5834
5209
  child.on("exit", () => {
5835
5210
  clearTimeout(timeout);
5836
- resolve23(null);
5211
+ resolve18(null);
5837
5212
  });
5838
5213
  });
5839
5214
  }
5840
5215
  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");
5216
+ const logsDir = logDir ?? resolve14(cwd, ".mind", "logs");
5217
+ mkdirSync6(logsDir, { recursive: true });
5218
+ const logPath = resolve14(logsDir, "mind.log");
5844
5219
  const logFd = openSync(logPath, "a");
5845
- const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
5220
+ const child = spawn3(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
5846
5221
  cwd,
5847
5222
  stdio: ["ignore", logFd, logFd],
5848
5223
  detached: true
@@ -5860,7 +5235,7 @@ function spawnDetached(cwd, port, logDir) {
5860
5235
  }
5861
5236
  const interval = setInterval(() => {
5862
5237
  try {
5863
- const content = readFileSync12(logPath, "utf-8");
5238
+ const content = readFileSync9(logPath, "utf-8");
5864
5239
  const match = content.match(/listening on :(\d+)/);
5865
5240
  if (match) {
5866
5241
  finish({ child, actualPort: parseInt(match[1], 10) });
@@ -5912,7 +5287,7 @@ async function verify2(port) {
5912
5287
  }
5913
5288
 
5914
5289
  // src/web/api/variants.ts
5915
- var app20 = new Hono20().get("/:name/variants", async (c) => {
5290
+ var app23 = new Hono23().get("/:name/variants", async (c) => {
5916
5291
  const name = c.req.param("name");
5917
5292
  const entry = findMind(name);
5918
5293
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -5952,11 +5327,11 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5952
5327
  const err = validateBranchName(variantName);
5953
5328
  if (err) return c.json({ error: err }, 400);
5954
5329
  const projectRoot = mindDir(mindName);
5955
- const variantDir = resolve20(projectRoot, ".variants", variantName);
5956
- if (existsSync14(variantDir)) {
5330
+ const variantDir = resolve15(projectRoot, ".variants", variantName);
5331
+ if (existsSync11(variantDir)) {
5957
5332
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
5958
5333
  }
5959
- mkdirSync9(resolve20(projectRoot, ".variants"), { recursive: true });
5334
+ mkdirSync7(resolve15(projectRoot, ".variants"), { recursive: true });
5960
5335
  try {
5961
5336
  await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
5962
5337
  } catch (e) {
@@ -5969,7 +5344,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5969
5344
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
5970
5345
  await exec(cmd, args, {
5971
5346
  cwd: variantDir,
5972
- env: { ...process.env, HOME: resolve20(variantDir, "home") }
5347
+ env: { ...process.env, HOME: resolve15(variantDir, "home") }
5973
5348
  });
5974
5349
  } else {
5975
5350
  await exec("npm", ["install"], { cwd: variantDir });
@@ -5979,7 +5354,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5979
5354
  return c.json({ error: `npm install failed: ${msg}` }, 500);
5980
5355
  }
5981
5356
  if (body.soul) {
5982
- writeFileSync9(resolve20(variantDir, "home/SOUL.md"), body.soul);
5357
+ writeFileSync7(resolve15(variantDir, "home/SOUL.md"), body.soul);
5983
5358
  }
5984
5359
  const variantPort = body.port ?? nextPort();
5985
5360
  const variant = {
@@ -6017,7 +5392,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
6017
5392
  } catch {
6018
5393
  }
6019
5394
  const projectRoot = mindDir(mindName);
6020
- if (existsSync14(variant.path)) {
5395
+ if (existsSync11(variant.path)) {
6021
5396
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
6022
5397
  if (status) {
6023
5398
  try {
@@ -6074,17 +5449,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
6074
5449
  } catch (e) {
6075
5450
  return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
6076
5451
  }
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);
5452
+ await cleanupVariant(mindName, variantName, projectRoot, variant.path);
6088
5453
  if (variantName === "upgrade") {
6089
5454
  try {
6090
5455
  const { computeTemplateHash: computeTemplateHash2 } = await import("./template-hash-BIMA4ILT.js");
@@ -6095,13 +5460,12 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
6095
5460
  console.error(`[daemon] failed to update template hash for ${mindName}:`, err);
6096
5461
  }
6097
5462
  }
6098
- chownMindDir(projectRoot, mindName);
6099
5463
  try {
6100
5464
  if (isIsolationEnabled()) {
6101
5465
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
6102
5466
  await exec(cmd, args, {
6103
5467
  cwd: projectRoot,
6104
- env: { ...process.env, HOME: resolve20(projectRoot, "home") }
5468
+ env: { ...process.env, HOME: resolve15(projectRoot, "home") }
6105
5469
  });
6106
5470
  } else {
6107
5471
  await exec("npm", ["install"], { cwd: projectRoot });
@@ -6136,41 +5500,22 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
6136
5500
  const variant = findVariant(mindName, variantName);
6137
5501
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
6138
5502
  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);
5503
+ await cleanupVariant(mindName, variantName, projectRoot, variant.path, { stop: true });
6159
5504
  return c.json({ ok: true });
6160
5505
  });
6161
- var variants_default = app20;
5506
+ var variants_default = app23;
6162
5507
 
6163
5508
  // 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")
5509
+ import { zValidator as zValidator8 } from "@hono/zod-validator";
5510
+ import { Hono as Hono24 } from "hono";
5511
+ import { z as z8 } from "zod";
5512
+ var createSchema2 = z8.object({
5513
+ name: z8.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
6169
5514
  });
6170
- var inviteSchema = z6.object({
6171
- username: z6.string().min(1)
5515
+ var inviteSchema = z8.object({
5516
+ username: z8.string().min(1)
6172
5517
  });
6173
- var app21 = new Hono21().get("/", async (c) => {
5518
+ var app24 = new Hono24().get("/", async (c) => {
6174
5519
  const user = c.get("user");
6175
5520
  const channels = await listChannels();
6176
5521
  const results = await Promise.all(
@@ -6181,7 +5526,7 @@ var app21 = new Hono21().get("/", async (c) => {
6181
5526
  })
6182
5527
  );
6183
5528
  return c.json(results);
6184
- }).post("/", zValidator6("json", createSchema), async (c) => {
5529
+ }).post("/", zValidator8("json", createSchema2), async (c) => {
6185
5530
  const user = c.get("user");
6186
5531
  const body = c.req.valid("json");
6187
5532
  try {
@@ -6214,7 +5559,7 @@ var app21 = new Hono21().get("/", async (c) => {
6214
5559
  if (!ch) return c.json({ error: "Channel not found" }, 404);
6215
5560
  const participants = await getParticipants(ch.id);
6216
5561
  return c.json(participants);
6217
- }).post("/:name/invite", zValidator6("json", inviteSchema), async (c) => {
5562
+ }).post("/:name/invite", zValidator8("json", inviteSchema), async (c) => {
6218
5563
  const name = c.req.param("name");
6219
5564
  const inviter = c.get("user");
6220
5565
  const { username } = c.req.valid("json");
@@ -6234,24 +5579,27 @@ var app21 = new Hono21().get("/", async (c) => {
6234
5579
  ]);
6235
5580
  return c.json({ ok: true });
6236
5581
  });
6237
- var channels_default2 = app21;
5582
+ var channels_default2 = app24;
6238
5583
 
6239
5584
  // 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) {
5585
+ import { zValidator as zValidator9 } from "@hono/zod-validator";
5586
+ import { Hono as Hono25 } from "hono";
5587
+ import { streamSSE as streamSSE6 } from "hono/streaming";
5588
+ import { z as z9 } from "zod";
5589
+ async function fanOutToMinds2(opts) {
6245
5590
  const participants = await getParticipants(opts.conversationId);
6246
5591
  const mindParticipants = participants.filter((p) => p.userType === "mind");
6247
5592
  const participantNames = participants.map((p) => p.username);
6248
5593
  const isDM = opts.isDM ?? participants.length === 2;
6249
5594
  const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
6250
- const { getMindManager: getMindManager2 } = await import("./mind-manager-3V2NXX4I.js");
5595
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-KMY4GA2J.js");
5596
+ const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
6251
5597
  const manager = getMindManager2();
6252
- const runningMinds = mindParticipants.map((ap) => {
5598
+ const sm = getSleepManagerIfReady();
5599
+ const targetMinds = mindParticipants.map((ap) => {
6253
5600
  const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
6254
- return manager.isRunning(key) ? ap.username : null;
5601
+ if (manager.isRunning(key) || sm?.isSleeping(ap.username)) return ap.username;
5602
+ return null;
6255
5603
  }).filter((n) => n !== null && n !== opts.senderName);
6256
5604
  function slugForMind(mindUsername) {
6257
5605
  return buildVoluteSlug({
@@ -6275,7 +5623,7 @@ async function fanOutToMinds(opts) {
6275
5623
  logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
6276
5624
  }
6277
5625
  }
6278
- for (const mindName of runningMinds) {
5626
+ for (const mindName of targetMinds) {
6279
5627
  const target = opts.targetName ? opts.targetName(mindName) : mindName;
6280
5628
  const channel = slugForMind(mindName);
6281
5629
  const typingMap = getTypingMap();
@@ -6293,18 +5641,18 @@ async function fanOutToMinds(opts) {
6293
5641
  });
6294
5642
  }
6295
5643
  }
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()
5644
+ var chatSchema = z9.object({
5645
+ message: z9.string().optional(),
5646
+ conversationId: z9.string().optional(),
5647
+ sender: z9.string().optional(),
5648
+ images: z9.array(
5649
+ z9.object({
5650
+ media_type: z9.string(),
5651
+ data: z9.string()
6304
5652
  })
6305
5653
  ).optional()
6306
5654
  });
6307
- var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
5655
+ var app25 = new Hono25().post("/:name/chat", zValidator9("json", chatSchema), async (c) => {
6308
5656
  const name = c.req.param("name");
6309
5657
  const [baseName] = name.split("@", 2);
6310
5658
  const entry = findMind(baseName);
@@ -6362,7 +5710,7 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
6362
5710
  }
6363
5711
  }
6364
5712
  await addMessage(conversationId, "user", senderName, contentBlocks);
6365
- await fanOutToMinds({
5713
+ await fanOutToMinds2({
6366
5714
  conversationId,
6367
5715
  contentBlocks,
6368
5716
  senderName,
@@ -6376,8 +5724,8 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
6376
5724
  if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
6377
5725
  return c.json({ error: "Conversation not found" }, 404);
6378
5726
  }
6379
- return streamSSE4(c, async (stream) => {
6380
- const unsubscribe = subscribe(conversationId, (event) => {
5727
+ return streamSSE6(c, async (stream) => {
5728
+ const unsubscribe = subscribe3(conversationId, (event) => {
6381
5729
  stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
6382
5730
  if (!stream.aborted) console.error("[chat] SSE write error:", err);
6383
5731
  });
@@ -6387,23 +5735,23 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
6387
5735
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
6388
5736
  });
6389
5737
  }, 15e3);
6390
- await new Promise((resolve23) => {
5738
+ await new Promise((resolve18) => {
6391
5739
  stream.onAbort(() => {
6392
5740
  unsubscribe();
6393
5741
  clearInterval(keepAlive);
6394
- resolve23();
5742
+ resolve18();
6395
5743
  });
6396
5744
  });
6397
5745
  });
6398
5746
  });
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()
5747
+ var unifiedChatSchema2 = z9.object({
5748
+ message: z9.string().optional(),
5749
+ conversationId: z9.string(),
5750
+ images: z9.array(z9.object({ media_type: z9.string(), data: z9.string() })).optional()
6403
5751
  });
6404
- var unifiedChatApp = new Hono22().post(
5752
+ var unifiedChatApp = new Hono25().post(
6405
5753
  "/chat",
6406
- zValidator7("json", unifiedChatSchema),
5754
+ zValidator9("json", unifiedChatSchema2),
6407
5755
  async (c) => {
6408
5756
  const user = c.get("user");
6409
5757
  const body = c.req.valid("json");
@@ -6425,7 +5773,7 @@ var unifiedChatApp = new Hono22().post(
6425
5773
  }
6426
5774
  await addMessage(body.conversationId, "user", senderName, contentBlocks);
6427
5775
  const isDM = conv.type === "dm";
6428
- await fanOutToMinds({
5776
+ await fanOutToMinds2({
6429
5777
  conversationId: body.conversationId,
6430
5778
  contentBlocks,
6431
5779
  senderName,
@@ -6437,18 +5785,18 @@ var unifiedChatApp = new Hono22().post(
6437
5785
  return c.json({ ok: true, conversationId: body.conversationId });
6438
5786
  }
6439
5787
  );
6440
- var chat_default = app22;
5788
+ var chat_default2 = app25;
6441
5789
 
6442
5790
  // 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()
5791
+ import { zValidator as zValidator10 } from "@hono/zod-validator";
5792
+ import { Hono as Hono26 } from "hono";
5793
+ import { z as z10 } from "zod";
5794
+ var createConvSchema = z10.object({
5795
+ title: z10.string().optional(),
5796
+ participantIds: z10.array(z10.number()).optional(),
5797
+ participantNames: z10.array(z10.string()).optional()
6450
5798
  });
6451
- var app23 = new Hono23().get("/:name/conversations", async (c) => {
5799
+ var app26 = new Hono26().get("/:name/conversations", async (c) => {
6452
5800
  const name = c.req.param("name");
6453
5801
  const user = c.get("user");
6454
5802
  let lookupId = user.id;
@@ -6459,7 +5807,7 @@ var app23 = new Hono23().get("/:name/conversations", async (c) => {
6459
5807
  const all = await listConversationsForUser(lookupId);
6460
5808
  const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
6461
5809
  return c.json(convs);
6462
- }).post("/:name/conversations", zValidator8("json", createConvSchema), async (c) => {
5810
+ }).post("/:name/conversations", zValidator10("json", createConvSchema), async (c) => {
6463
5811
  const name = c.req.param("name");
6464
5812
  const user = c.get("user");
6465
5813
  const body = c.req.valid("json");
@@ -6533,18 +5881,18 @@ var app23 = new Hono23().get("/:name/conversations", async (c) => {
6533
5881
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
6534
5882
  return c.json({ ok: true });
6535
5883
  });
6536
- var conversations_default = app23;
5884
+ var conversations_default2 = app26;
6537
5885
 
6538
5886
  // 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)
5887
+ import { zValidator as zValidator11 } from "@hono/zod-validator";
5888
+ import { Hono as Hono27 } from "hono";
5889
+ import { streamSSE as streamSSE7 } from "hono/streaming";
5890
+ import { z as z11 } from "zod";
5891
+ var createSchema3 = z11.object({
5892
+ title: z11.string().optional(),
5893
+ participantNames: z11.array(z11.string()).min(1)
6546
5894
  });
6547
- var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
5895
+ var app27 = new Hono27().use("*", authMiddleware).get("/", async (c) => {
6548
5896
  const user = c.get("user");
6549
5897
  const convs = await listConversationsWithParticipants(user.id);
6550
5898
  return c.json(convs);
@@ -6556,7 +5904,7 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
6556
5904
  }
6557
5905
  const msgs = await getMessages(id);
6558
5906
  return c.json(msgs);
6559
- }).post("/", zValidator9("json", createSchema2), async (c) => {
5907
+ }).post("/", zValidator11("json", createSchema3), async (c) => {
6560
5908
  const user = c.get("user");
6561
5909
  const body = c.req.valid("json");
6562
5910
  const participantIds = /* @__PURE__ */ new Set();
@@ -6592,8 +5940,8 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
6592
5940
  if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
6593
5941
  return c.json({ error: "Conversation not found" }, 404);
6594
5942
  }
6595
- return streamSSE5(c, async (stream) => {
6596
- const unsubscribe = subscribe(conversationId, (event) => {
5943
+ return streamSSE7(c, async (stream) => {
5944
+ const unsubscribe = subscribe3(conversationId, (event) => {
6597
5945
  stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
6598
5946
  if (!stream.aborted) console.error("[chat] SSE write error:", err);
6599
5947
  });
@@ -6603,11 +5951,11 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
6603
5951
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
6604
5952
  });
6605
5953
  }, 15e3);
6606
- await new Promise((resolve23) => {
5954
+ await new Promise((resolve18) => {
6607
5955
  stream.onAbort(() => {
6608
5956
  unsubscribe();
6609
5957
  clearInterval(keepAlive);
6610
- resolve23();
5958
+ resolve18();
6611
5959
  });
6612
5960
  });
6613
5961
  });
@@ -6618,12 +5966,12 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
6618
5966
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
6619
5967
  return c.json({ ok: true });
6620
5968
  });
6621
- var user_conversations_default = app24;
5969
+ var user_conversations_default = app27;
6622
5970
 
6623
5971
  // src/web/app.ts
6624
5972
  var httpLog = logger_default.child("http");
6625
- var app25 = new Hono25();
6626
- app25.onError((err, c) => {
5973
+ var app28 = new Hono28();
5974
+ app28.onError((err, c) => {
6627
5975
  if (err instanceof HTTPException) {
6628
5976
  return err.getResponse();
6629
5977
  }
@@ -6634,10 +5982,10 @@ app25.onError((err, c) => {
6634
5982
  });
6635
5983
  return c.json({ error: "Internal server error" }, 500);
6636
5984
  });
6637
- app25.notFound((c) => {
5985
+ app28.notFound((c) => {
6638
5986
  return c.json({ error: "Not found" }, 404);
6639
5987
  });
6640
- app25.use("*", async (c, next) => {
5988
+ app28.use("*", async (c, next) => {
6641
5989
  const start = Date.now();
6642
5990
  await next();
6643
5991
  const duration = Date.now() - start;
@@ -6648,7 +5996,7 @@ app25.use("*", async (c, next) => {
6648
5996
  httpLog.debug("request", data);
6649
5997
  }
6650
5998
  });
6651
- app25.get("/api/health", (c) => {
5999
+ app28.get("/api/health", (c) => {
6652
6000
  let version = "unknown";
6653
6001
  let cached = null;
6654
6002
  try {
@@ -6663,19 +6011,35 @@ app25.get("/api/health", (c) => {
6663
6011
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
6664
6012
  });
6665
6013
  });
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;
6014
+ app28.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
6015
+ app28.use("/api/*", csrf());
6016
+ app28.use("/api/activity/*", authMiddleware);
6017
+ app28.use("/api/minds/*", authMiddleware);
6018
+ app28.use("/api/conversations/*", authMiddleware);
6019
+ app28.use("/api/volute/*", authMiddleware);
6020
+ app28.use("/api/system/*", authMiddleware);
6021
+ app28.use("/api/env/*", authMiddleware);
6022
+ app28.use("/api/prompts/*", authMiddleware);
6023
+ app28.use("/api/skills/*", authMiddleware);
6024
+ app28.use("/api/v1/*", authMiddleware);
6025
+ app28.route("/pages", pages_default);
6026
+ 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);
6027
+ app28.route("/api/v1/minds", minds_default);
6028
+ app28.route("/api/v1/minds", typing_default);
6029
+ app28.route("/api/v1/minds", variants_default);
6030
+ app28.route("/api/v1/minds", files_default);
6031
+ app28.route("/api/v1/minds", env_default);
6032
+ app28.route("/api/v1/minds", mind_skills_default);
6033
+ app28.route("/api/v1/minds", connectors_default);
6034
+ app28.route("/api/v1/minds", schedules_default);
6035
+ app28.route("/api/v1/minds", logs_default);
6036
+ app28.route("/api/v1/system", system_default);
6037
+ app28.route("/api/v1/system", update_default);
6038
+ app28.route("/api/v1/prompts", prompts_default);
6039
+ app28.route("/api/v1/skills", skills_default);
6040
+ app28.route("/api/v1/env", sharedEnvApp);
6041
+ app28.route("/api/v1/channels", channels_default2);
6042
+ var app_default = app28;
6679
6043
 
6680
6044
  // src/web/server.ts
6681
6045
  var MIME_TYPES2 = {
@@ -6692,20 +6056,20 @@ async function startServer({
6692
6056
  hostname = "127.0.0.1"
6693
6057
  }) {
6694
6058
  let assetsDir = "";
6695
- let searchDir = dirname2(new URL(import.meta.url).pathname);
6059
+ let searchDir = dirname(new URL(import.meta.url).pathname);
6696
6060
  for (let i = 0; i < 5; i++) {
6697
- const candidate = resolve21(searchDir, "dist", "web-assets");
6698
- if (existsSync15(candidate)) {
6061
+ const candidate = resolve16(searchDir, "dist", "web-assets");
6062
+ if (existsSync12(candidate)) {
6699
6063
  assetsDir = candidate;
6700
6064
  break;
6701
6065
  }
6702
- searchDir = dirname2(searchDir);
6066
+ searchDir = dirname(searchDir);
6703
6067
  }
6704
6068
  if (assetsDir) {
6705
6069
  app_default.get("*", async (c) => {
6706
6070
  const urlPath = new URL(c.req.url).pathname;
6707
6071
  if (urlPath.startsWith("/api/")) return c.notFound();
6708
- const filePath = resolve21(assetsDir, urlPath.slice(1));
6072
+ const filePath = resolve16(assetsDir, urlPath.slice(1));
6709
6073
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
6710
6074
  const s = await stat3(filePath).catch(() => null);
6711
6075
  if (s?.isFile()) {
@@ -6714,7 +6078,7 @@ async function startServer({
6714
6078
  const body = await readFile3(filePath);
6715
6079
  return c.body(body, 200, { "Content-Type": mime });
6716
6080
  }
6717
- const indexPath = resolve21(assetsDir, "index.html");
6081
+ const indexPath = resolve16(assetsDir, "index.html");
6718
6082
  const indexStat = await stat3(indexPath).catch(() => null);
6719
6083
  if (indexStat?.isFile()) {
6720
6084
  const body = await readFile3(indexPath, "utf-8");
@@ -6724,10 +6088,10 @@ async function startServer({
6724
6088
  });
6725
6089
  }
6726
6090
  const server = serve({ fetch: app_default.fetch, port, hostname });
6727
- await new Promise((resolve23, reject) => {
6091
+ await new Promise((resolve18, reject) => {
6728
6092
  server.on("listening", () => {
6729
6093
  logger_default.info("Volute UI running", { hostname, port });
6730
- resolve23();
6094
+ resolve18();
6731
6095
  });
6732
6096
  server.on("error", (err) => {
6733
6097
  reject(err);
@@ -6738,14 +6102,17 @@ async function startServer({
6738
6102
 
6739
6103
  // src/daemon.ts
6740
6104
  if (!process.env.VOLUTE_HOME) {
6741
- process.env.VOLUTE_HOME = resolve22(homedir2(), ".volute");
6105
+ process.env.VOLUTE_HOME = resolve17(homedir2(), ".volute");
6106
+ }
6107
+ if (process.env.VOLUTE_TIMEZONE && !process.env.TZ) {
6108
+ process.env.TZ = process.env.VOLUTE_TIMEZONE;
6742
6109
  }
6743
6110
  async function startDaemon(opts) {
6744
6111
  const { port, hostname } = opts;
6745
6112
  const myPid = String(process.pid);
6746
6113
  const home = voluteHome();
6747
6114
  if (!opts.foreground) {
6748
- const rotatingLog = new RotatingLog(resolve22(home, "daemon.log"));
6115
+ const rotatingLog = new RotatingLog(resolve17(home, "daemon.log"));
6749
6116
  logger_default.setOutput((line) => rotatingLog.write(`${line}
6750
6117
  `));
6751
6118
  const write = (...args) => rotatingLog.write(`${format(...args)}
@@ -6755,9 +6122,9 @@ async function startDaemon(opts) {
6755
6122
  console.warn = write;
6756
6123
  console.info = write;
6757
6124
  }
6758
- const DAEMON_PID_PATH = resolve22(home, "daemon.pid");
6759
- const DAEMON_JSON_PATH = resolve22(home, "daemon.json");
6760
- mkdirSync10(home, { recursive: true });
6125
+ const DAEMON_PID_PATH = resolve17(home, "daemon.pid");
6126
+ const DAEMON_JSON_PATH = resolve17(home, "daemon.json");
6127
+ mkdirSync8(home, { recursive: true });
6761
6128
  migrateAgentsToMinds();
6762
6129
  try {
6763
6130
  await ensureSharedRepo();
@@ -6785,8 +6152,8 @@ async function startDaemon(opts) {
6785
6152
  }
6786
6153
  throw err;
6787
6154
  }
6788
- writeFileSync10(DAEMON_PID_PATH, myPid, { mode: 420 });
6789
- writeFileSync10(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
6155
+ writeFileSync8(DAEMON_PID_PATH, myPid, { mode: 420 });
6156
+ writeFileSync8(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
6790
6157
  `, {
6791
6158
  mode: 420
6792
6159
  });
@@ -6800,6 +6167,9 @@ async function startDaemon(opts) {
6800
6167
  mailPoller.start();
6801
6168
  const tokenBudget = initTokenBudget();
6802
6169
  tokenBudget.start();
6170
+ const sleepManager = initSleepManager();
6171
+ sleepManager.start();
6172
+ const unsubscribeWebhook = initWebhook();
6803
6173
  const registry = readRegistry();
6804
6174
  for (const entry of registry) {
6805
6175
  try {
@@ -6815,6 +6185,20 @@ async function startDaemon(opts) {
6815
6185
  const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
6816
6186
  while (queue.length > 0) {
6817
6187
  const entry = queue.shift();
6188
+ if (sleepManager.isSleeping(entry.name)) {
6189
+ try {
6190
+ const dir = mindDir(entry.name);
6191
+ const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
6192
+ await connectors.startConnectors(entry.name, dir, entry.port, daemonPort);
6193
+ scheduler.loadSchedules(entry.name);
6194
+ } catch (err) {
6195
+ logger_default.error(
6196
+ `failed to start connectors for sleeping mind ${entry.name}`,
6197
+ logger_default.errorData(err)
6198
+ );
6199
+ }
6200
+ continue;
6201
+ }
6818
6202
  try {
6819
6203
  await startMindFull(entry.name);
6820
6204
  } catch (err) {
@@ -6842,8 +6226,15 @@ async function startDaemon(opts) {
6842
6226
  });
6843
6227
  await Promise.all(workers);
6844
6228
  }
6229
+ import("./cloud-sync-PI47U2LT.js").then(
6230
+ ({ consumeQueuedMessages }) => consumeQueuedMessages().catch((err) => {
6231
+ logger_default.warn("failed to consume queued cloud messages", logger_default.errorData(err));
6232
+ })
6233
+ ).catch((err) => {
6234
+ logger_default.warn("failed to load cloud-sync module", logger_default.errorData(err));
6235
+ });
6845
6236
  try {
6846
- const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-TFS2U5CF.js");
6237
+ const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-LKABEJSA.js");
6847
6238
  backfillTemplateHashes();
6848
6239
  notifyVersionUpdate().catch((err) => {
6849
6240
  logger_default.warn("failed to send version update notifications", logger_default.errorData(err));
@@ -6860,15 +6251,15 @@ async function startDaemon(opts) {
6860
6251
  logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
6861
6252
  function cleanup() {
6862
6253
  try {
6863
- if (readFileSync13(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
6864
- unlinkSync2(DAEMON_PID_PATH);
6254
+ if (readFileSync10(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
6255
+ unlinkSync(DAEMON_PID_PATH);
6865
6256
  }
6866
6257
  } catch {
6867
6258
  }
6868
6259
  try {
6869
- const data = JSON.parse(readFileSync13(DAEMON_JSON_PATH, "utf-8"));
6260
+ const data = JSON.parse(readFileSync10(DAEMON_JSON_PATH, "utf-8"));
6870
6261
  if (data.token === token) {
6871
- unlinkSync2(DAEMON_JSON_PATH);
6262
+ unlinkSync(DAEMON_JSON_PATH);
6872
6263
  }
6873
6264
  } catch {
6874
6265
  }
@@ -6890,6 +6281,9 @@ async function startDaemon(opts) {
6890
6281
  try {
6891
6282
  safe("stopAllWatchers", stopAllWatchers);
6892
6283
  safe("stopAllActivityTrackers", stopAll);
6284
+ safe("unsubscribeWebhook", unsubscribeWebhook);
6285
+ safe("sleepManager.stop", () => sleepManager.stop());
6286
+ safe("sleepManager.saveState", () => sleepManager.saveState());
6893
6287
  safe("scheduler.stop", () => scheduler.stop());
6894
6288
  safe("scheduler.saveState", () => scheduler.saveState());
6895
6289
  safe("mailPoller.stop", () => mailPoller.stop());