volute 0.45.1 → 0.46.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 (120) hide show
  1. package/README.md +3 -3
  2. package/dist/{activity-events-OMCSM7XW.js → activity-events-NF6VWTJQ.js} +2 -2
  3. package/dist/{ai-service-534WUSDL.js → ai-service-UODCCE4O.js} +3 -3
  4. package/dist/{archive-6R4TZMYI.js → archive-WS2L2VIQ.js} +2 -2
  5. package/dist/{auth-FRZJVQCS.js → auth-CCFAU4GQ.js} +3 -3
  6. package/dist/{avatar-image-BNWEXSA2.js → avatar-image-6KY6UU4D.js} +3 -3
  7. package/dist/{backup-OZXHL3JS.js → backup-TST5C2YN.js} +7 -7
  8. package/dist/{chat-I4EDDIQC.js → chat-ZIQQS25U.js} +2 -2
  9. package/dist/{chunk-ZIR7HSAX.js → chunk-4JOQZPCE.js} +1 -1
  10. package/dist/chunk-4YU7HF4L.js +237 -0
  11. package/dist/{chunk-JSRMEY52.js → chunk-5ZQZDDIK.js} +10 -5
  12. package/dist/{chunk-2BONPXAF.js → chunk-6EMWJZBK.js} +1 -1
  13. package/dist/{chunk-LAXUUY7O.js → chunk-A7WQZKOU.js} +2 -2
  14. package/dist/{chunk-H7IYFRAL.js → chunk-ALKYQULU.js} +10 -5
  15. package/dist/{chunk-5AUKCNPE.js → chunk-BJZ4PV7R.js} +8 -8
  16. package/dist/{chunk-KP3KWNG6.js → chunk-CDPBTTZO.js} +2 -2
  17. package/dist/{chunk-SQL7M4HJ.js → chunk-DRJ24GXK.js} +2 -2
  18. package/dist/{chunk-77BVGCTZ.js → chunk-E3DA4HYV.js} +9 -9
  19. package/dist/{chunk-322KE457.js → chunk-F7H6ATM4.js} +1 -1
  20. package/dist/{chunk-KVKP6VQ5.js → chunk-FXORA5JG.js} +12 -3
  21. package/dist/{chunk-3ERIL673.js → chunk-H2X54EHC.js} +1 -1
  22. package/dist/{chunk-TSK5PI4L.js → chunk-IHKGWTGT.js} +1 -1
  23. package/dist/{chunk-U4FNXDEJ.js → chunk-JO5IO4FH.js} +422 -168
  24. package/dist/{chunk-OXXTPILE.js → chunk-JZ62SYWR.js} +1 -1
  25. package/dist/{chunk-IUXTYXFI.js → chunk-K6T43YBD.js} +3 -3
  26. package/dist/{chunk-Y6OPHHGM.js → chunk-KGIXX4NM.js} +4 -4
  27. package/dist/{chunk-HVU45COX.js → chunk-KQSFTPIM.js} +1 -1
  28. package/dist/{chunk-CYI534DJ.js → chunk-OV7ZQ2GH.js} +2 -2
  29. package/dist/{chunk-CU6OFXMM.js → chunk-PFZBNKUH.js} +9 -0
  30. package/dist/{clock-K43IMROX.js → chunk-PJ45FQPT.js} +21 -16
  31. package/dist/{chunk-QMJJWTNL.js → chunk-QCN24XEN.js} +1 -1
  32. package/dist/{chunk-UMI4UIJG.js → chunk-R77O2WMK.js} +1 -1
  33. package/dist/{chunk-IEWZ7VD7.js → chunk-RRNXJOKB.js} +2 -2
  34. package/dist/{chunk-FHWUQZXE.js → chunk-RXEEYDIB.js} +1 -1
  35. package/dist/{chunk-Q2XBKQ2D.js → chunk-TQJ6ZYVY.js} +2 -2
  36. package/dist/{chunk-V4GYPD5L.js → chunk-YXSUMLTC.js} +46 -269
  37. package/dist/{chunk-5GT7URXJ.js → chunk-ZDZISKCL.js} +1 -1
  38. package/dist/{chunk-ZMIMSIIT.js → chunk-ZNMCCHCP.js} +48 -2
  39. package/dist/cli.js +20 -20
  40. package/dist/clock-RHS2JEDL.js +16 -0
  41. package/dist/{cloud-sync-Y7LQPRE3.js → cloud-sync-46RS4SDA.js} +17 -16
  42. package/dist/{conversations-G4LYMHAA.js → conversations-CJOOCGNW.js} +3 -3
  43. package/dist/{create-2YY7IPMS.js → create-IOV4GCRX.js} +1 -1
  44. package/dist/{daemon-restart-ABP54YN6.js → daemon-restart-VFEI57UO.js} +6 -6
  45. package/dist/daemon.js +392 -270
  46. package/dist/{db-JFLCJCUG.js → db-MF2JNRUW.js} +1 -1
  47. package/dist/{delete-BFMIGAIB.js → delete-HPLC7ETU.js} +1 -1
  48. package/dist/delivery-manager-JINZRG2W.js +33 -0
  49. package/dist/{delivery-router-6DE3BPHO.js → delivery-router-N4A7ZC5T.js} +2 -2
  50. package/dist/{down-XCAZXWLB.js → down-U2CZ23M4.js} +4 -4
  51. package/dist/echo-text-VQPB5L2X.js +33 -0
  52. package/dist/{exec-4GWHAHS3.js → exec-G3ZRST4Z.js} +2 -2
  53. package/dist/{export-ID7YUSLZ.js → export-LY723I3Z.js} +5 -5
  54. package/dist/{extensions-AYSPKE33.js → extensions-ZELQLTSR.js} +17 -16
  55. package/dist/{files-BQ2WB5KC.js → files-4LN3EN7Q.js} +2 -2
  56. package/dist/{import-SKID37QX.js → import-42YNQBF4.js} +5 -5
  57. package/dist/{isolation-5MB3J73F.js → isolation-YMGZZ4E5.js} +2 -2
  58. package/dist/{join-RXPXGV4L.js → join-ZJV7W6MO.js} +4 -0
  59. package/dist/{login-LM4NF4ZB.js → login-M6OJUPQ2.js} +1 -1
  60. package/dist/{logout-CTJVIZGA.js → logout-WQBWO77G.js} +1 -1
  61. package/dist/message-delivery-R7JSQGYX.js +37 -0
  62. package/dist/{mind-KLHC7XYR.js → mind-LHKS55DI.js} +12 -12
  63. package/dist/{mind-activity-tracker-BGHJE7XA.js → mind-activity-tracker-3QRNJVR4.js} +3 -3
  64. package/dist/mind-service-NB2JMWQZ.js +39 -0
  65. package/dist/mind-sleep-74TGOQBC.js +83 -0
  66. package/dist/{package-BMHV7UHN.js → package-DOD6CPO6.js} +2 -1
  67. package/dist/{registry-NUJAGLQJ.js → registry-OUZBZ3Q4.js} +5 -1
  68. package/dist/{sandbox-72MWV4JL.js → sandbox-ZJ7QA6Z6.js} +3 -3
  69. package/dist/scheduler-WVKII3LA.js +31 -0
  70. package/dist/{seed-4MKQ5QKG.js → seed-CNYIHPOH.js} +1 -1
  71. package/dist/{seed-cmd-LCXMR7VV.js → seed-cmd-AVNUNLZV.js} +2 -2
  72. package/dist/{seed-create-N2BF5LQX.js → seed-create-ANXGYSCC.js} +5 -5
  73. package/dist/{seed-sprout-6ZDP23YI.js → seed-sprout-NIUFTX32.js} +10 -8
  74. package/dist/{send-O6W53VBM.js → send-SPE6DVNJ.js} +8 -2
  75. package/dist/{service-AQVPUANO.js → service-UOCLXDWR.js} +3 -3
  76. package/dist/{service-install-4QNUDKLM.js → service-install-4HHFDDWY.js} +4 -4
  77. package/dist/{setup-3O6O2JZ7.js → setup-QT2A25SY.js} +6 -6
  78. package/dist/{setup-73YBJZQL.js → setup-Y2EJI3GG.js} +2 -2
  79. package/dist/skills/imagegen/scripts/imagegen.ts +100 -40
  80. package/dist/skills/orientation/SKILL.md +1 -1
  81. package/dist/skills/volute-mind/SKILL.md +13 -9
  82. package/dist/skills/volute-mind/references/sleep.md +8 -1
  83. package/dist/{skills-ND4ZFYUW.js → skills-AKCFA7BN.js} +4 -4
  84. package/dist/sleep-manager-G6TXSDP7.js +35 -0
  85. package/dist/{spirit-II3HOERM.js → spirit-RPBLLUQ5.js} +7 -7
  86. package/dist/{sprout-4KUG76AQ.js → sprout-SMX42ZBP.js} +1 -1
  87. package/dist/{src-XFY3XDD6.js → src-OJHARHTF.js} +28 -17
  88. package/dist/{status-E5DQNCAO.js → status-SWRVGXBI.js} +4 -4
  89. package/dist/system-chat-NDAW6YP7.js +37 -0
  90. package/dist/{tailscale-QAWVKQKU.js → tailscale-3XOKOW7T.js} +1 -1
  91. package/dist/{turn-tracker-QQIVVXBI.js → turn-tracker-FOIBNUUA.js} +5 -4
  92. package/dist/{up-LG56MPBI.js → up-PS6PFS75.js} +5 -5
  93. package/dist/{update-7TYVH5FM.js → update-SWAYZRWJ.js} +4 -4
  94. package/dist/{update-check-VSCSJ7KX.js → update-check-PQ2MZ47K.js} +2 -2
  95. package/dist/{version-notify-CODJOUW2.js → version-notify-C5CH7CVP.js} +24 -19
  96. package/dist/{volute-config-TS62GS6A.js → volute-config-A52FHWKM.js} +5 -1
  97. package/dist/web-assets/assets/{index-Dac11sKB.js → index-CU6XWICM.js} +15 -15
  98. package/dist/web-assets/index.html +1 -1
  99. package/package.json +2 -1
  100. package/templates/_base/home/VOLUTE.md +6 -2
  101. package/templates/_base/src/lib/context-breakdown.ts +57 -67
  102. package/templates/_base/src/lib/file-handler.ts +3 -3
  103. package/templates/_base/src/lib/router.ts +21 -25
  104. package/templates/_base/src/lib/startup.ts +2 -0
  105. package/templates/_base/src/lib/types.ts +2 -2
  106. package/templates/_base/src/lib/volute-server.ts +2 -2
  107. package/templates/claude/src/agent.ts +147 -46
  108. package/templates/claude/src/lib/message-channel.ts +23 -0
  109. package/templates/claude/src/lib/session-reaper.ts +68 -0
  110. package/templates/claude/src/server.ts +1 -0
  111. package/templates/codex/src/agent.ts +80 -46
  112. package/templates/pi/src/agent.ts +73 -45
  113. package/dist/delivery-manager-R5STGLXB.js +0 -32
  114. package/dist/echo-text-NQXYYYCU.js +0 -32
  115. package/dist/message-delivery-IUAXRPFE.js +0 -34
  116. package/dist/mind-service-BF2LUQMH.js +0 -36
  117. package/dist/mind-sleep-YMBR3I35.js +0 -47
  118. package/dist/scheduler-YHJRRV6E.js +0 -30
  119. package/dist/sleep-manager-YAHDOIU5.js +0 -34
  120. package/dist/system-chat-E65T2HSW.js +0 -34
package/dist/daemon.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  readBackupState,
7
7
  resticVersion,
8
8
  runBackup
9
- } from "./chunk-KVKP6VQ5.js";
9
+ } from "./chunk-FXORA5JG.js";
10
10
  import {
11
11
  acceptPending,
12
12
  formatFileSize,
@@ -14,25 +14,25 @@ import {
14
14
  rejectPending,
15
15
  stageFile,
16
16
  validateFilePath
17
- } from "./chunk-UMI4UIJG.js";
17
+ } from "./chunk-R77O2WMK.js";
18
18
  import {
19
19
  findOpenClawSession,
20
20
  importOpenClawConnectors,
21
21
  importPiSession,
22
22
  parseNameFromIdentity
23
- } from "./chunk-LAXUUY7O.js";
23
+ } from "./chunk-A7WQZKOU.js";
24
24
  import {
25
25
  checkForUpdate,
26
26
  checkForUpdateCached,
27
27
  getCurrentVersion
28
- } from "./chunk-2BONPXAF.js";
28
+ } from "./chunk-6EMWJZBK.js";
29
29
  import {
30
30
  normalizeAvatar,
31
31
  safeResolveWithinBase
32
- } from "./chunk-SQL7M4HJ.js";
32
+ } from "./chunk-DRJ24GXK.js";
33
33
  import {
34
34
  isHomeOnlyArchive
35
- } from "./chunk-QMJJWTNL.js";
35
+ } from "./chunk-QCN24XEN.js";
36
36
  import {
37
37
  computeTemplateHash
38
38
  } from "./chunk-QMJCNZ6C.js";
@@ -42,7 +42,7 @@ import {
42
42
  fixModelEscapes,
43
43
  getPlatformDriver,
44
44
  routeOutboundBridge
45
- } from "./chunk-77BVGCTZ.js";
45
+ } from "./chunk-E3DA4HYV.js";
46
46
  import {
47
47
  getBridgeConfig,
48
48
  readBridgesConfig,
@@ -51,7 +51,7 @@ import {
51
51
  resolveChannelMapping,
52
52
  setBridgeConfig,
53
53
  setChannelMapping
54
- } from "./chunk-FHWUQZXE.js";
54
+ } from "./chunk-RXEEYDIB.js";
55
55
  import {
56
56
  MindStartupError,
57
57
  RestartTracker,
@@ -71,7 +71,7 @@ import {
71
71
  ensureSystemChannel,
72
72
  formatHoldNotice,
73
73
  formatNotices,
74
- generateSystemReply,
74
+ generateSystemFallbackReply,
75
75
  getAllDiscoveredExtensions,
76
76
  getAllDiscoveredExtensionsDetailed,
77
77
  getDeliveryManager,
@@ -107,20 +107,21 @@ import {
107
107
  requireAdminOrSystem,
108
108
  requireSelf,
109
109
  setExtensionEnabled,
110
+ shouldGenerateSystemFallback,
110
111
  startMindFull,
111
112
  stopMindFull,
112
113
  syncProviderToMinds,
113
114
  uninstallNpmExtension,
114
115
  writeSystemsConfig
115
- } from "./chunk-U4FNXDEJ.js";
116
+ } from "./chunk-JO5IO4FH.js";
116
117
  import {
117
118
  loadMergedEnv,
118
119
  mindEnvPath,
119
120
  readEnv,
120
121
  sharedEnvPath,
121
122
  writeEnv
122
- } from "./chunk-5GT7URXJ.js";
123
- import "./chunk-5AUKCNPE.js";
123
+ } from "./chunk-ZDZISKCL.js";
124
+ import "./chunk-BJZ4PV7R.js";
124
125
  import {
125
126
  applyInitFiles,
126
127
  applyTemplateHomeFiles,
@@ -149,7 +150,7 @@ import {
149
150
  syncBuiltinSkills,
150
151
  uninstallSkill,
151
152
  updateSkill
152
- } from "./chunk-Y6OPHHGM.js";
153
+ } from "./chunk-KGIXX4NM.js";
153
154
  import {
154
155
  addMessage,
155
156
  createChannel,
@@ -177,51 +178,54 @@ import {
177
178
  listConversationsForUser,
178
179
  listConversationsWithParticipants,
179
180
  markConversationRead,
180
- publish,
181
+ publish as publish2,
181
182
  setConversationPrivate,
182
183
  subscribe as subscribe2,
183
184
  updateChannelSettings
184
- } from "./chunk-JSRMEY52.js";
185
+ } from "./chunk-5ZQZDDIK.js";
185
186
  import {
186
187
  readVoluteConfig,
187
188
  writeVoluteConfig
188
- } from "./chunk-CU6OFXMM.js";
189
+ } from "./chunk-PFZBNKUH.js";
189
190
  import {
190
191
  isSandboxEnabled,
191
192
  wrapForSandbox
192
- } from "./chunk-OXXTPILE.js";
193
+ } from "./chunk-JZ62SYWR.js";
193
194
  import {
194
195
  getActiveMinds,
195
196
  onMindEvent,
196
197
  stopAll
197
- } from "./chunk-HVU45COX.js";
198
+ } from "./chunk-KQSFTPIM.js";
198
199
  import {
199
200
  extractTextContent
200
- } from "./chunk-ZIR7HSAX.js";
201
+ } from "./chunk-4JOQZPCE.js";
201
202
  import {
202
- PROMPT_DEFAULTS,
203
- PROMPT_KEYS,
204
203
  assignSession,
205
204
  completeOrphanedTurns,
206
205
  completeTurn,
207
206
  createTurn,
208
207
  getActiveTurnId,
209
- getMindPromptDefaults,
210
- getPrompt,
211
- getPromptIfCustom,
212
208
  getToolUseEventId,
213
209
  initSummarizer,
214
210
  markErrored,
215
- publish as publish2,
211
+ publish as publish3,
216
212
  subscribe as subscribe3,
217
213
  subscribeAll,
218
- substitute,
219
214
  summarizeOrphanedTurns,
220
215
  summarizeTurn,
221
216
  takeErrored,
222
217
  trackToolUse
223
- } from "./chunk-V4GYPD5L.js";
224
- import "./chunk-Q2XBKQ2D.js";
218
+ } from "./chunk-YXSUMLTC.js";
219
+ import {
220
+ ORIENTATION_MARKER,
221
+ PROMPT_DEFAULTS,
222
+ PROMPT_KEYS,
223
+ getMindPromptDefaults,
224
+ getPrompt,
225
+ getPromptIfCustom,
226
+ substitute
227
+ } from "./chunk-4YU7HF4L.js";
228
+ import "./chunk-TQJ6ZYVY.js";
225
229
  import {
226
230
  chownMindDir,
227
231
  createMindUser,
@@ -232,9 +236,10 @@ import {
232
236
  isIsolationEnabled,
233
237
  resolveVoluteBin,
234
238
  wrapForIsolation
235
- } from "./chunk-3ERIL673.js";
239
+ } from "./chunk-H2X54EHC.js";
236
240
  import {
237
241
  addCustomModel,
242
+ aiCompleteUtility,
238
243
  buildCustomModel,
239
244
  getAiConfig,
240
245
  getAvailableModels,
@@ -253,12 +258,12 @@ import {
253
258
  setProviderRefreshHook,
254
259
  setUtilityModel,
255
260
  unqualifyModelId
256
- } from "./chunk-TSK5PI4L.js";
261
+ } from "./chunk-IHKGWTGT.js";
257
262
  import {
258
263
  isSetupComplete,
259
264
  readGlobalConfig,
260
265
  writeGlobalConfig
261
- } from "./chunk-322KE457.js";
266
+ } from "./chunk-F7H6ATM4.js";
262
267
  import {
263
268
  approveUser,
264
269
  changePassword,
@@ -277,11 +282,14 @@ import {
277
282
  syncMindProfile,
278
283
  updateUserProfile,
279
284
  verifyUser
280
- } from "./chunk-CYI534DJ.js";
285
+ } from "./chunk-OV7ZQ2GH.js";
281
286
  import {
282
287
  broadcast,
288
+ getEventsSince,
289
+ nextEventId,
290
+ publish,
283
291
  subscribe
284
- } from "./chunk-ZMIMSIIT.js";
292
+ } from "./chunk-ZNMCCHCP.js";
285
293
  import {
286
294
  logBuffer,
287
295
  logger_default
@@ -290,6 +298,7 @@ import {
290
298
  buildVoluteSlug
291
299
  } from "./chunk-LGNUFVMR.js";
292
300
  import {
301
+ SPIRIT_NAME,
293
302
  addMind,
294
303
  addVariant,
295
304
  daemonLoopback,
@@ -305,6 +314,7 @@ import {
305
314
  readAllMinds,
306
315
  readRegistry,
307
316
  removeMind,
317
+ resolveMindDir,
308
318
  setMindRunning,
309
319
  setMindStage,
310
320
  setMindTemplate,
@@ -313,7 +323,7 @@ import {
313
323
  validateMindName,
314
324
  voluteHome,
315
325
  voluteSystemDir
316
- } from "./chunk-H7IYFRAL.js";
326
+ } from "./chunk-ALKYQULU.js";
317
327
  import {
318
328
  activity,
319
329
  channels,
@@ -338,9 +348,12 @@ import { format } from "util";
338
348
  import { CronExpressionParser } from "cron-parser";
339
349
  var blog = logger_default.child("backup");
340
350
  var DEFAULT_BACKUP_SCHEDULE = "0 3 * * *";
351
+ var NOTIFY_EVERY_N_FAILURES = 5;
341
352
  var BackupManager = class {
342
353
  interval = null;
343
- lastFiredMinute = -1;
354
+ /** Epoch-ms of the most recent scheduled fire this process has serviced. */
355
+ lastServicedFireMs = -1;
356
+ consecutiveFailures = 0;
344
357
  warnedUnconfigured = false;
345
358
  start() {
346
359
  this.interval = setInterval(() => {
@@ -352,9 +365,16 @@ var BackupManager = class {
352
365
  this.interval = null;
353
366
  }
354
367
  /**
355
- * True at most once per scheduled fire minute (records the minute it
356
- * consumed), and only when backups are enabled and runnable. Not a pure
357
- * predicate — a second call in the same minute returns false.
368
+ * True at most once per scheduled fire (records the fire it consumed), and
369
+ * only when backups are enabled and runnable. Not a pure predicate — a second
370
+ * call for the same fire returns false.
371
+ *
372
+ * Catch-up: if the machine was asleep or the daemon was down across a
373
+ * scheduled fire (a laptop at 3am), the fire is picked up on the next tick so
374
+ * long as it falls after the last successful run (`lastRun` in
375
+ * backup-state.json). Without an anchor (no successful backup yet) only the
376
+ * exact on-time minute fires, so freshly enabling backups doesn't trigger an
377
+ * immediate run.
358
378
  */
359
379
  isDue(now = /* @__PURE__ */ new Date()) {
360
380
  const config2 = readGlobalConfig().backup;
@@ -369,28 +389,82 @@ var BackupManager = class {
369
389
  this.warnedUnconfigured = false;
370
390
  const cron = config2.schedule || DEFAULT_BACKUP_SCHEDULE;
371
391
  const epochMinute = Math.floor(now.getTime() / 6e4);
372
- if (epochMinute === this.lastFiredMinute) return false;
373
- let prevMinute;
392
+ let prevFireMs;
374
393
  try {
375
394
  const interval = CronExpressionParser.parse(cron, { currentDate: now });
376
- prevMinute = Math.floor(interval.prev().toDate().getTime() / 6e4);
395
+ prevFireMs = interval.prev().toDate().getTime();
377
396
  } catch (err) {
378
397
  blog.warn(`invalid backup cron "${cron}"`, logger_default.errorData(err));
379
398
  return false;
380
399
  }
381
- if (prevMinute !== epochMinute) return false;
382
- this.lastFiredMinute = epochMinute;
400
+ if (prevFireMs <= this.lastServicedFireMs) return false;
401
+ const lastRunMs = this.readLastRunMs();
402
+ if (lastRunMs !== null && prevFireMs <= lastRunMs) return false;
403
+ const onTime = Math.floor(prevFireMs / 6e4) === epochMinute;
404
+ if (!onTime) {
405
+ if (lastRunMs === null) return false;
406
+ blog.warn(
407
+ `missed scheduled backup fire at ${new Date(prevFireMs).toISOString()}; running catch-up`
408
+ );
409
+ }
410
+ this.lastServicedFireMs = prevFireMs;
383
411
  return true;
384
412
  }
413
+ readLastRunMs() {
414
+ const lastRun = readBackupState().lastRun;
415
+ if (!lastRun) return null;
416
+ const ms = Date.parse(lastRun);
417
+ return Number.isNaN(ms) ? null : ms;
418
+ }
385
419
  async tick() {
386
420
  if (!this.isDue()) return;
387
421
  blog.info("scheduled backup starting");
388
422
  try {
389
423
  await runBackup();
424
+ this.recordSuccess();
390
425
  } catch (err) {
391
426
  blog.error("scheduled backup failed", logger_default.errorData(err));
427
+ await this.recordFailure(err);
392
428
  }
393
429
  }
430
+ recordSuccess() {
431
+ this.consecutiveFailures = 0;
432
+ }
433
+ /**
434
+ * Track consecutive scheduled-backup failures and actively notify the operator
435
+ * on the 1st and then every Nth. Returns whether it notified (for tests).
436
+ * Notification failures are swallowed — surfacing a backup failure must never
437
+ * take the daemon's backup loop down with it.
438
+ */
439
+ async recordFailure(err) {
440
+ this.consecutiveFailures += 1;
441
+ const n = this.consecutiveFailures;
442
+ if (n !== 1 && n % NOTIFY_EVERY_N_FAILURES !== 0) return false;
443
+ const reason = err instanceof Error ? err.message : String(err);
444
+ const summary = n === 1 ? `Scheduled backup failed: ${reason}` : `Scheduled backup has failed ${n} times in a row: ${reason}`;
445
+ try {
446
+ await publish({
447
+ type: "backup_failed",
448
+ mind: SPIRIT_NAME,
449
+ summary,
450
+ metadata: { consecutiveFailures: n, error: reason }
451
+ });
452
+ } catch (pubErr) {
453
+ blog.error("failed to publish backup-failure activity", logger_default.errorData(pubErr));
454
+ }
455
+ try {
456
+ const { sendSystemMessage } = await import("./system-chat-NDAW6YP7.js");
457
+ await sendSystemMessage(
458
+ SPIRIT_NAME,
459
+ `${summary}
460
+
461
+ Backups are not being saved. Check \`volute backup status\` and the Backups settings tab.`
462
+ );
463
+ } catch (msgErr) {
464
+ blog.error("failed to send backup-failure system message", logger_default.errorData(msgErr));
465
+ }
466
+ return true;
467
+ }
394
468
  };
395
469
  var backupManager = null;
396
470
  function initBackupManager() {
@@ -725,6 +799,26 @@ async function cleanExpiredLogs() {
725
799
  await db.delete(mindHistory).where(and(eq(mindHistory.type, "log"), lt(mindHistory.created_at, cutoff)));
726
800
  }
727
801
 
802
+ // packages/daemon/src/lib/daemon/maintenance.ts
803
+ var MAINTENANCE_INTERVAL_MS = 60 * 60 * 1e3;
804
+ async function runMaintenance() {
805
+ try {
806
+ await cleanExpiredSessions();
807
+ } catch (err) {
808
+ logger_default.warn("maintenance: failed to clean expired sessions", logger_default.errorData(err));
809
+ }
810
+ try {
811
+ await cleanExpiredLogs();
812
+ } catch (err) {
813
+ logger_default.warn("maintenance: failed to clean expired logs", logger_default.errorData(err));
814
+ }
815
+ }
816
+ function startMaintenanceInterval(intervalMs = MAINTENANCE_INTERVAL_MS, task = runMaintenance) {
817
+ const handle = setInterval(() => void task(), intervalMs);
818
+ handle.unref?.();
819
+ return handle;
820
+ }
821
+
728
822
  // packages/daemon/src/web/api/system.ts
729
823
  import { getOAuthProvider, getOAuthProviders } from "@earendil-works/pi-ai/oauth";
730
824
  import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
@@ -1472,10 +1566,10 @@ function getCachedApiKey(provider) {
1472
1566
  var keyRefreshTimer = null;
1473
1567
  var slog = logger_default.child("ai-keys");
1474
1568
  var oauthHealth = /* @__PURE__ */ new Map();
1475
- function needsRefresh(oauth) {
1569
+ function needsRefresh(oauth, now = Date.now()) {
1476
1570
  const expires = oauth.expires;
1477
- if (!expires || Date.now() >= expires) return true;
1478
- return expires - Date.now() < 15 * 60 * 1e3;
1571
+ if (!expires || now >= expires) return true;
1572
+ return expires - now < 15 * 60 * 1e3;
1479
1573
  }
1480
1574
  async function refreshApiKeyCache() {
1481
1575
  const ai = getAiConfig();
@@ -1693,17 +1787,17 @@ var authenticated = new Hono3().use(authMiddleware).post("/change-password", zVa
1693
1787
  if (!AVATAR_MIME[ext]) {
1694
1788
  return c.json({ error: "Invalid file type (png, jpg, gif, webp only)" }, 400);
1695
1789
  }
1696
- let buffer2 = Buffer.from(await file.arrayBuffer());
1790
+ let buffer = Buffer.from(await file.arrayBuffer());
1697
1791
  let finalExt = ext;
1698
- const normalized = await normalizeAvatar(buffer2);
1792
+ const normalized = await normalizeAvatar(buffer);
1699
1793
  if (normalized) {
1700
- buffer2 = normalized.buffer;
1794
+ buffer = normalized.buffer;
1701
1795
  finalExt = normalized.ext;
1702
1796
  }
1703
1797
  const dir = avatarsDir();
1704
1798
  mkdirSync2(dir, { recursive: true });
1705
1799
  const filename = `avatar-${user.id}${finalExt}`;
1706
- writeFileSync2(resolve3(dir, filename), buffer2);
1800
+ writeFileSync2(resolve3(dir, filename), buffer);
1707
1801
  if (user.avatar && user.avatar !== filename) {
1708
1802
  const oldPath = resolve3(dir, user.avatar);
1709
1803
  rmSync(oldPath, { force: true });
@@ -2165,7 +2259,7 @@ var app4 = new Hono5().post("/:platform/inbound", zValidator4("json", inboundSch
2165
2259
  }
2166
2260
  const participants = await getParticipants(channel.id);
2167
2261
  if (!participants.some((p) => p.userId === puppet.id)) {
2168
- const { addParticipant } = await import("./conversations-G4LYMHAA.js");
2262
+ const { addParticipant } = await import("./conversations-CJOOCGNW.js");
2169
2263
  await addParticipant(channel.id, puppet.id);
2170
2264
  }
2171
2265
  const contentBlocks = body.content;
@@ -2449,7 +2543,7 @@ async function notifyMind(mindName, message) {
2449
2543
  const entry = await findMind(mindName);
2450
2544
  if (!entry) return;
2451
2545
  try {
2452
- const { sendSystemMessage } = await import("./system-chat-E65T2HSW.js");
2546
+ const { sendSystemMessage } = await import("./system-chat-NDAW6YP7.js");
2453
2547
  await sendSystemMessage(mindName, message);
2454
2548
  } catch (err) {
2455
2549
  logger_default.warn(`[file-sharing] notify mind ${mindName} failed`, logger_default.errorData(err));
@@ -2618,11 +2712,11 @@ var app9 = new Hono11().post("/:name/avatar", requireSelf(), async (c) => {
2618
2712
  if (!AVATAR_MIME2[ext]) {
2619
2713
  return c.json({ error: "Invalid file type (png, jpg, gif, webp only)" }, 400);
2620
2714
  }
2621
- let buffer2 = Buffer.from(await file.arrayBuffer());
2715
+ let buffer = Buffer.from(await file.arrayBuffer());
2622
2716
  let finalExt = ext;
2623
- const normalized = await normalizeAvatar(buffer2);
2717
+ const normalized = await normalizeAvatar(buffer);
2624
2718
  if (normalized) {
2625
- buffer2 = normalized.buffer;
2719
+ buffer = normalized.buffer;
2626
2720
  finalExt = normalized.ext;
2627
2721
  }
2628
2722
  const dir = entry.dir ?? mindDir(name);
@@ -2636,7 +2730,7 @@ var app9 = new Hono11().post("/:name/avatar", requireSelf(), async (c) => {
2636
2730
  if (oldAvatarPath) rmSync2(oldAvatarPath, { force: true });
2637
2731
  }
2638
2732
  mkdirSync3(homeDir, { recursive: true });
2639
- writeFileSync3(avatarPath, buffer2);
2733
+ writeFileSync3(avatarPath, buffer);
2640
2734
  const profile = config2.profile ?? {};
2641
2735
  profile.avatar = filename;
2642
2736
  config2.profile = profile;
@@ -3301,6 +3395,7 @@ var app12 = new Hono15().get("/:name/skills", async (c) => {
3301
3395
  const dir = entry.dir ?? mindDir(name);
3302
3396
  try {
3303
3397
  const result = await installSkill(name, dir, skillId);
3398
+ await chownMindDir(dir, name);
3304
3399
  return c.json({ ok: true, ...result });
3305
3400
  } catch (e) {
3306
3401
  const msg = e instanceof Error ? e.message : String(e);
@@ -3319,6 +3414,7 @@ var app12 = new Hono15().get("/:name/skills", async (c) => {
3319
3414
  const dir = entry.dir ?? mindDir(name);
3320
3415
  try {
3321
3416
  const result = await updateSkill(name, dir, skillId);
3417
+ await chownMindDir(dir, name);
3322
3418
  return c.json(result);
3323
3419
  } catch (e) {
3324
3420
  const msg = e instanceof Error ? e.message : String(e);
@@ -3351,6 +3447,7 @@ var app12 = new Hono15().get("/:name/skills", async (c) => {
3351
3447
  const dir = entry.dir ?? mindDir(name);
3352
3448
  try {
3353
3449
  await uninstallSkill(name, dir, skillName);
3450
+ await chownMindDir(dir, name);
3354
3451
  } catch (e) {
3355
3452
  const msg = e instanceof Error ? e.message : String(e);
3356
3453
  return c.json({ error: msg }, 400);
@@ -3462,7 +3559,7 @@ async function handleMindEvent(mind, event) {
3462
3559
  if (!turnId) {
3463
3560
  llog.warn(`skipping turn tracking for ${mind}: createTurn failed`);
3464
3561
  } else {
3465
- publish2(mind, { mind, type: "turn_created", turnId });
3562
+ publish3(mind, { mind, type: "turn_created", turnId });
3466
3563
  if (event.session) await assignSession(mind, turnId, event.session);
3467
3564
  try {
3468
3565
  await linkPendingInbound(mind, turnId, event.channel);
@@ -3508,7 +3605,7 @@ async function handleMindEvent(mind, event) {
3508
3605
  llog.error("failed to link tool_result to turn", logger_default.errorData(err));
3509
3606
  }
3510
3607
  }
3511
- publish2(mind, {
3608
+ publish3(mind, {
3512
3609
  mind,
3513
3610
  type: event.type,
3514
3611
  session: event.session,
@@ -3595,35 +3692,50 @@ async function completeTurnAndSummarize(mind, event, insertedId) {
3595
3692
  // packages/daemon/src/lib/mind/consolidate.ts
3596
3693
  import { readdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
3597
3694
  import { resolve as resolve8 } from "path";
3598
- async function consolidateMemory(mindDir2) {
3695
+ var cLog = logger_default.child("consolidate");
3696
+ var MAX_CONSOLIDATION_INPUT_CHARS = 2e5;
3697
+ function readDailyLogs(memoryDir) {
3698
+ let files;
3699
+ try {
3700
+ files = readdirSync(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
3701
+ } catch {
3702
+ return [];
3703
+ }
3704
+ const logs = [];
3705
+ for (const filename of files) {
3706
+ const date = filename.replace(".md", "");
3707
+ const content = readFileSync6(resolve8(memoryDir, filename), "utf-8").trim();
3708
+ if (content) logs.push(`### ${date}
3709
+
3710
+ ${content}`);
3711
+ }
3712
+ return logs;
3713
+ }
3714
+ function boundLogText(logs, maxChars = MAX_CONSOLIDATION_INPUT_CHARS) {
3715
+ const kept = [];
3716
+ let total = 0;
3717
+ for (let i = logs.length - 1; i >= 0; i--) {
3718
+ const len = logs[i].length + (kept.length > 0 ? 2 : 0);
3719
+ if (total + len > maxChars) break;
3720
+ kept.unshift(logs[i]);
3721
+ total += len;
3722
+ }
3723
+ if (kept.length === 0 && logs.length > 0) {
3724
+ return logs[logs.length - 1].slice(-maxChars);
3725
+ }
3726
+ return kept.join("\n\n");
3727
+ }
3728
+ async function consolidateMemory(mindDir2, complete = aiCompleteUtility) {
3599
3729
  const soulPath = resolve8(mindDir2, "home/SOUL.md");
3600
3730
  const memoryPath = resolve8(mindDir2, "home/MEMORY.md");
3601
3731
  const memoryDir = resolve8(mindDir2, "home/memory");
3602
3732
  const soul = readFileSync6(soulPath, "utf-8");
3603
- const logs = [];
3604
- try {
3605
- const files = readdirSync(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
3606
- for (const filename of files) {
3607
- const date = filename.replace(".md", "");
3608
- const content2 = readFileSync6(resolve8(memoryDir, filename), "utf-8").trim();
3609
- if (content2) {
3610
- logs.push(`### ${date}
3611
-
3612
- ${content2}`);
3613
- }
3614
- }
3615
- } catch {
3616
- }
3733
+ const logs = readDailyLogs(memoryDir);
3617
3734
  if (logs.length === 0) {
3618
- console.log("No daily logs found.");
3735
+ cLog.info("No daily logs found; skipping memory consolidation.");
3619
3736
  return;
3620
3737
  }
3621
- const apiKey = process.env.ANTHROPIC_API_KEY;
3622
- if (!apiKey) {
3623
- console.error("ANTHROPIC_API_KEY not set, skipping memory consolidation.");
3624
- return;
3625
- }
3626
- console.log("Consolidating memory from daily logs...");
3738
+ cLog.info("Consolidating memory from daily logs...");
3627
3739
  const userMessage = [
3628
3740
  "You have daily logs from a previous environment but no long-term memory file yet.",
3629
3741
  "Please review the daily logs below and produce consolidated MEMORY.md content.",
@@ -3631,35 +3743,15 @@ ${content2}`);
3631
3743
  "",
3632
3744
  "## Daily logs",
3633
3745
  "",
3634
- logs.join("\n\n")
3746
+ boundLogText(logs)
3635
3747
  ].join("\n");
3636
- const res = await fetch("https://api.anthropic.com/v1/messages", {
3637
- method: "POST",
3638
- headers: {
3639
- "Content-Type": "application/json",
3640
- "x-api-key": apiKey,
3641
- "anthropic-version": "2023-06-01"
3642
- },
3643
- body: JSON.stringify({
3644
- model: "claude-sonnet-4-20250514",
3645
- max_tokens: 4096,
3646
- system: soul,
3647
- messages: [{ role: "user", content: userMessage }]
3648
- })
3649
- });
3650
- if (!res.ok) {
3651
- const body = await res.text();
3652
- console.error(`Anthropic API error (${res.status}): ${body}`);
3653
- return;
3654
- }
3655
- const data = await res.json();
3656
- const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
3748
+ const content = (await complete(soul, userMessage))?.trim();
3657
3749
  if (content) {
3658
3750
  writeFileSync5(memoryPath, `${content}
3659
3751
  `);
3660
- console.log("MEMORY.md created successfully.");
3752
+ cLog.info("MEMORY.md created successfully.");
3661
3753
  } else {
3662
- console.warn("Warning: No content produced.");
3754
+ cLog.warn("No content produced; skipping memory consolidation.");
3663
3755
  }
3664
3756
  }
3665
3757
 
@@ -3706,7 +3798,7 @@ async function cleanupVariant(variantName, projectRoot, variantPath, opts) {
3706
3798
  logger_default.warn(`failed to stop variant ${variantName}`, logger_default.errorData(err));
3707
3799
  }
3708
3800
  }
3709
- const { findMind: findMind2 } = await import("./registry-NUJAGLQJ.js");
3801
+ const { findMind: findMind2 } = await import("./registry-OUZBZ3Q4.js");
3710
3802
  const variantEntry = await findMind2(variantName);
3711
3803
  const branchName = variantEntry?.branch ?? variantName;
3712
3804
  if (existsSync6(variantPath)) {
@@ -3935,7 +4027,7 @@ async function getMindStatus(name, port, registryRunning) {
3935
4027
  const manager = getMindManager();
3936
4028
  let status = "stopped";
3937
4029
  try {
3938
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-YAHDOIU5.js");
4030
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
3939
4031
  if (getSleepManagerIfReady2()?.isSleeping(name)) {
3940
4032
  status = "sleeping";
3941
4033
  }
@@ -4430,7 +4522,7 @@ var app13 = new Hono16().post("/", requireAdminOrSystem, zValidator6("json", cre
4430
4522
  applyInitFiles(dest);
4431
4523
  const { publicKeyPem } = generateIdentity(dest);
4432
4524
  {
4433
- const { readGlobalConfig: readGlobal } = await import("./setup-73YBJZQL.js");
4525
+ const { readGlobalConfig: readGlobal } = await import("./setup-Y2EJI3GG.js");
4434
4526
  const mindDefaults = readGlobal().mindDefaults;
4435
4527
  const config2 = readVoluteConfig(dest);
4436
4528
  if (!config2) throw new Error("Failed to read volute.json after identity generation");
@@ -4510,10 +4602,9 @@ var app13 = new Hono16().post("/", requireAdminOrSystem, zValidator6("json", cre
4510
4602
  rmSync4(resolve10(dest, ".git"), { recursive: true, force: true });
4511
4603
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
4512
4604
  }
4513
- await chownMindDir(dest, name);
4514
4605
  if (body.stage === "seed") {
4515
4606
  const descLine = body.description ? `
4516
- The human who planted you described you as: "${body.description}"
4607
+ Your creator described you as: "${body.description}"
4517
4608
  ` : "";
4518
4609
  const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
4519
4610
  const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
@@ -4521,7 +4612,7 @@ The human who planted you described you as: "${body.description}"
4521
4612
  }
4522
4613
  let skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : getStandardSkillsWithExtensions());
4523
4614
  if (body.stage === "seed" && !body.skills) {
4524
- const { isImagegenEnabled } = await import("./setup-73YBJZQL.js");
4615
+ const { isImagegenEnabled } = await import("./setup-Y2EJI3GG.js");
4525
4616
  if (isImagegenEnabled()) {
4526
4617
  skillSet = [...skillSet, "imagegen"];
4527
4618
  }
@@ -4539,7 +4630,7 @@ The human who planted you described you as: "${body.description}"
4539
4630
  try {
4540
4631
  const spiritEntry = await findMind("volute");
4541
4632
  if (spiritEntry) {
4542
- const { spiritDir } = await import("./spirit-II3HOERM.js");
4633
+ const { spiritDir } = await import("./spirit-RPBLLUQ5.js");
4543
4634
  const sDir = spiritEntry.dir ?? spiritDir();
4544
4635
  const spiritConfig = readVoluteConfig(sDir) ?? {};
4545
4636
  const schedules = spiritConfig.schedules ?? [];
@@ -4554,7 +4645,7 @@ The human who planted you described you as: "${body.description}"
4554
4645
  });
4555
4646
  spiritConfig.schedules = schedules;
4556
4647
  writeVoluteConfig(sDir, spiritConfig);
4557
- const { getScheduler: getScheduler2 } = await import("./scheduler-YHJRRV6E.js");
4648
+ const { getScheduler: getScheduler2 } = await import("./scheduler-WVKII3LA.js");
4558
4649
  getScheduler2().loadSchedules("volute", sDir);
4559
4650
  }
4560
4651
  }
@@ -4572,6 +4663,7 @@ The human who planted you described you as: "${body.description}"
4572
4663
  writeFileSync7(resolve10(dest, "home/MEMORY.md"), customMemory);
4573
4664
  }
4574
4665
  }
4666
+ await chownMindDir(dest, name);
4575
4667
  publishPublicKey(name, publicKeyPem).catch(
4576
4668
  (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
4577
4669
  );
@@ -4819,7 +4911,7 @@ ${user.trimEnd()}
4819
4911
  const manager = getMindManager();
4820
4912
  try {
4821
4913
  if (context?.type === "reload") {
4822
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-YAHDOIU5.js");
4914
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
4823
4915
  const sleepState = getSleepManagerIfReady2()?.getState(name);
4824
4916
  if (sleepState?.sleeping) {
4825
4917
  logger_default.info(`skipping reload for ${name} during sleep \u2014 will apply on next wake`);
@@ -4946,7 +5038,7 @@ ${user.trimEnd()}
4946
5038
  const name = c.req.param("name");
4947
5039
  const entry = await findMind(name);
4948
5040
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4949
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-YAHDOIU5.js");
5041
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
4950
5042
  const sm = getSleepManagerIfReady2();
4951
5043
  if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
4952
5044
  return c.json(sm.getState(name));
@@ -4954,7 +5046,7 @@ ${user.trimEnd()}
4954
5046
  const name = c.req.param("name");
4955
5047
  const entry = await findMind(name);
4956
5048
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4957
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-YAHDOIU5.js");
5049
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
4958
5050
  const sm = getSleepManagerIfReady2();
4959
5051
  if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
4960
5052
  if (sm.isSleeping(name)) return c.json({ error: "Mind is already sleeping" }, 409);
@@ -4974,7 +5066,7 @@ ${user.trimEnd()}
4974
5066
  const name = c.req.param("name");
4975
5067
  const entry = await findMind(name);
4976
5068
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4977
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-YAHDOIU5.js");
5069
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
4978
5070
  const sm = getSleepManagerIfReady2();
4979
5071
  if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
4980
5072
  const sleepState = sm.getState(name);
@@ -4989,7 +5081,7 @@ ${user.trimEnd()}
4989
5081
  const name = c.req.param("name");
4990
5082
  const entry = await findMind(name);
4991
5083
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4992
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-YAHDOIU5.js");
5084
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
4993
5085
  const sm = getSleepManagerIfReady2();
4994
5086
  if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
4995
5087
  const flushed = await sm.flushQueuedMessages(name);
@@ -5017,7 +5109,7 @@ ${user.trimEnd()}
5017
5109
  }
5018
5110
  config2.profile = profile;
5019
5111
  writeVoluteConfig(dir, config2);
5020
- const { syncMindProfile: syncMindProfile2 } = await import("./auth-FRZJVQCS.js");
5112
+ const { syncMindProfile: syncMindProfile2 } = await import("./auth-CCFAU4GQ.js");
5021
5113
  await syncMindProfile2(name, profile);
5022
5114
  broadcast({ type: "profile_updated", mind: name, summary: `${name} profile updated` });
5023
5115
  return c.json({ ok: true });
@@ -5027,7 +5119,6 @@ ${user.trimEnd()}
5027
5119
  if (!entry) return c.json({ error: "Mind not found" }, 404);
5028
5120
  if (entry.stage !== "seed") return c.json({ output: "" });
5029
5121
  const db = await getDb();
5030
- const ORIENTATION_MARKER = "You don't have a soul yet";
5031
5122
  const rawCreator = Number(process.env.VOLUTE_NURTURE_CREATOR_MINUTES);
5032
5123
  const creatorThreshold = Number.isNaN(rawCreator) ? 5 : rawCreator;
5033
5124
  const rawSpirit = Number(process.env.VOLUTE_NURTURE_SPIRIT_MINUTES);
@@ -5064,7 +5155,7 @@ ${user.trimEnd()}
5064
5155
  const config2 = readVoluteConfig(dir);
5065
5156
  const displayNameSet = !!config2?.profile?.displayName;
5066
5157
  const avatarSet = !!config2?.profile?.avatar;
5067
- const { isImagegenEnabled } = await import("./setup-73YBJZQL.js");
5158
+ const { isImagegenEnabled } = await import("./setup-Y2EJI3GG.js");
5068
5159
  const imagegenEnabled = isImagegenEnabled();
5069
5160
  const done = [];
5070
5161
  const remaining = [];
@@ -5102,7 +5193,7 @@ ${user.trimEnd()}
5102
5193
  try {
5103
5194
  const spiritEntry = await findMind("volute");
5104
5195
  if (spiritEntry) {
5105
- const { spiritDir } = await import("./spirit-II3HOERM.js");
5196
+ const { spiritDir } = await import("./spirit-RPBLLUQ5.js");
5106
5197
  const sDir = spiritEntry.dir ?? spiritDir();
5107
5198
  const spiritConfig = readVoluteConfig(sDir);
5108
5199
  if (spiritConfig?.schedules) {
@@ -5110,7 +5201,7 @@ ${user.trimEnd()}
5110
5201
  spiritConfig.schedules = spiritConfig.schedules.filter((s) => s.id !== nurtureId);
5111
5202
  if (spiritConfig.schedules.length === 0) spiritConfig.schedules = void 0;
5112
5203
  writeVoluteConfig(sDir, spiritConfig);
5113
- const { getScheduler: getScheduler2 } = await import("./scheduler-YHJRRV6E.js");
5204
+ const { getScheduler: getScheduler2 } = await import("./scheduler-WVKII3LA.js");
5114
5205
  getScheduler2().loadSchedules("volute", sDir);
5115
5206
  }
5116
5207
  }
@@ -5545,7 +5636,7 @@ ${user.trimEnd()}
5545
5636
  }
5546
5637
  writeVoluteConfig(dir, existing);
5547
5638
  if (body.unescapeNewlines !== void 0) {
5548
- const { clearEchoTextCache } = await import("./echo-text-NQXYYYCU.js");
5639
+ const { clearEchoTextCache } = await import("./echo-text-VQPB5L2X.js");
5549
5640
  clearEchoTextCache(name);
5550
5641
  }
5551
5642
  const needsConfigJson = body.model !== void 0 || body.thinkingLevel !== void 0 || body.maxThinkingTokens !== void 0 || body.compaction !== void 0;
@@ -5622,7 +5713,7 @@ ${user.trimEnd()}
5622
5713
  }
5623
5714
  if (entry.mindType === "spirit" && body.model !== void 0) {
5624
5715
  try {
5625
- const { readGlobalConfig: readGlobalConfig2, writeGlobalConfig: writeGlobalConfig2 } = await import("./setup-73YBJZQL.js");
5716
+ const { readGlobalConfig: readGlobalConfig2, writeGlobalConfig: writeGlobalConfig2 } = await import("./setup-Y2EJI3GG.js");
5626
5717
  const globalConfig = readGlobalConfig2();
5627
5718
  globalConfig.spiritModel = body.model;
5628
5719
  writeGlobalConfig2(globalConfig);
@@ -5650,7 +5741,7 @@ ${user.trimEnd()}
5650
5741
  if (!body.systemPrompt || !body.message) {
5651
5742
  return c.json({ error: "systemPrompt and message required" }, 400);
5652
5743
  }
5653
- const { aiComplete: aiCompleteFn, isAiConfigured } = await import("./ai-service-534WUSDL.js");
5744
+ const { aiComplete: aiCompleteFn, isAiConfigured } = await import("./ai-service-UODCCE4O.js");
5654
5745
  if (!isAiConfigured()) {
5655
5746
  return c.json({ error: "AI service not configured" }, 503);
5656
5747
  }
@@ -5999,76 +6090,93 @@ var slog2 = logger_default.child("schedules");
5999
6090
  function readSchedules(dir) {
6000
6091
  return readVoluteConfig(dir)?.schedules ?? [];
6001
6092
  }
6002
- function writeSchedules(name, dir, schedules) {
6003
- const config2 = readVoluteConfig(dir) ?? {};
6004
- config2.schedules = schedules.length > 0 ? schedules : void 0;
6005
- writeVoluteConfig(dir, config2);
6006
- getScheduler().loadSchedules(name, dir);
6007
- getSleepManagerIfReady()?.invalidateSleepConfig(name);
6008
- fireWebhook({
6009
- event: "schedule_changed",
6010
- mind: name,
6011
- data: { schedules }
6012
- });
6013
- }
6014
- var app15 = new Hono18().get("/:name/clock/status", requireSelf(), async (c) => {
6015
- const name = c.req.param("name");
6016
- const entry = await findMind(name);
6017
- if (!entry) return c.json({ error: "Mind not found" }, 404);
6018
- const dir = entry.dir ?? mindDir(name);
6019
- const sleepManager = getSleepManagerIfReady();
6020
- const sleepState = sleepManager?.getState(name) ?? null;
6021
- const sleepConfig = sleepManager?.getSleepConfig(name) ?? null;
6022
- const schedules = readSchedules(dir);
6023
- const now = /* @__PURE__ */ new Date();
6093
+ var UPCOMING_HORIZON_MS = 24 * 60 * 60 * 1e3;
6094
+ function computeClockEvents(schedules, sleepState, sleepConfig, now) {
6024
6095
  const upcoming = [];
6025
6096
  const previous = [];
6097
+ const effectiveWake = sleepState?.sleeping ? sleepState.scheduledWakeAt ?? sleepState.voluntaryWakeAt : null;
6098
+ const effectiveWakeDate = effectiveWake ? new Date(effectiveWake) : null;
6099
+ const sleepFate = (s, fireDate) => {
6100
+ if (!effectiveWakeDate || fireDate >= effectiveWakeDate) return {};
6101
+ const ws = s.whileSleeping ?? "queue";
6102
+ if (ws === "skip") return { willSkip: true };
6103
+ if (ws === "trigger-wake") return {};
6104
+ return { willQueue: true };
6105
+ };
6026
6106
  for (const s of schedules) {
6027
6107
  if (!s.enabled) continue;
6028
6108
  if (s.fireAt) {
6029
6109
  const fireDate = new Date(s.fireAt);
6030
6110
  if (fireDate >= now) {
6031
- upcoming.push({ id: s.id, at: fireDate.toISOString(), type: "timer" });
6111
+ upcoming.push({
6112
+ id: s.id,
6113
+ at: fireDate.toISOString(),
6114
+ type: "timer",
6115
+ ...sleepFate(s, fireDate)
6116
+ });
6032
6117
  }
6033
6118
  } else if (s.cron) {
6034
6119
  try {
6035
- const interval = CronExpressionParser3.parse(s.cron);
6036
- const next = interval.next().toDate();
6037
- upcoming.push({ id: s.id, at: next.toISOString(), type: "cron" });
6120
+ const next = CronExpressionParser3.parse(s.cron, { currentDate: now }).next().toDate();
6121
+ upcoming.push({ id: s.id, at: next.toISOString(), type: "cron", ...sleepFate(s, next) });
6038
6122
  } catch {
6039
- slog2.warn(`invalid cron "${s.cron}" for schedule "${s.id}" of ${name}`);
6123
+ slog2.warn(`invalid cron "${s.cron}" for schedule "${s.id}"`);
6040
6124
  }
6041
6125
  try {
6042
- const prevInterval = CronExpressionParser3.parse(s.cron);
6043
- const prev = prevInterval.prev().toDate();
6126
+ const prev = CronExpressionParser3.parse(s.cron, { currentDate: now }).prev().toDate();
6044
6127
  previous.push({ id: s.id, at: prev.toISOString() });
6045
6128
  } catch {
6046
6129
  }
6047
6130
  }
6048
6131
  }
6049
6132
  if (sleepState?.sleeping) {
6050
- if (sleepState.scheduledWakeAt) {
6051
- upcoming.push({ id: "sleep", at: sleepState.scheduledWakeAt, type: "cron" });
6133
+ if (effectiveWake) {
6134
+ upcoming.push({ id: "wake", at: effectiveWake, type: "cron" });
6052
6135
  }
6053
6136
  if (sleepState.sleepingSince) {
6054
6137
  previous.push({ id: "sleep", at: sleepState.sleepingSince });
6055
6138
  }
6056
6139
  } else if (sleepConfig?.enabled && sleepConfig.schedule) {
6057
6140
  try {
6058
- const sleepInterval = CronExpressionParser3.parse(sleepConfig.schedule.sleep);
6059
- const nextSleep = sleepInterval.next().toDate();
6141
+ const nextSleep = CronExpressionParser3.parse(sleepConfig.schedule.sleep, { currentDate: now }).next().toDate();
6060
6142
  upcoming.push({ id: "sleep", at: nextSleep.toISOString(), type: "cron" });
6061
6143
  } catch {
6062
6144
  }
6063
6145
  try {
6064
- const prevWakeInterval = CronExpressionParser3.parse(sleepConfig.schedule.wake);
6065
- const prevWake = prevWakeInterval.prev().toDate();
6066
- previous.push({ id: "sleep", at: prevWake.toISOString() });
6146
+ const prevWake = CronExpressionParser3.parse(sleepConfig.schedule.wake, { currentDate: now }).prev().toDate();
6147
+ previous.push({ id: "wake", at: prevWake.toISOString() });
6067
6148
  } catch {
6068
6149
  }
6069
6150
  }
6070
- upcoming.sort((a, b) => a.at.localeCompare(b.at));
6151
+ const horizon = now.getTime() + UPCOMING_HORIZON_MS;
6152
+ const withinHorizon = upcoming.filter((e) => new Date(e.at).getTime() <= horizon);
6153
+ withinHorizon.sort((a, b) => a.at.localeCompare(b.at));
6071
6154
  previous.sort((a, b) => b.at.localeCompare(a.at));
6155
+ return { upcoming: withinHorizon, previous };
6156
+ }
6157
+ function writeSchedules(name, dir, schedules) {
6158
+ const config2 = readVoluteConfig(dir) ?? {};
6159
+ config2.schedules = schedules.length > 0 ? schedules : void 0;
6160
+ writeVoluteConfig(dir, config2);
6161
+ getScheduler().loadSchedules(name, dir);
6162
+ getSleepManagerIfReady()?.invalidateSleepConfig(name);
6163
+ fireWebhook({
6164
+ event: "schedule_changed",
6165
+ mind: name,
6166
+ data: { schedules }
6167
+ });
6168
+ }
6169
+ var app15 = new Hono18().get("/:name/clock/status", requireSelf(), async (c) => {
6170
+ const name = c.req.param("name");
6171
+ const entry = await findMind(name);
6172
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
6173
+ const dir = entry.dir ?? mindDir(name);
6174
+ const sleepManager = getSleepManagerIfReady();
6175
+ const sleepState = sleepManager?.getState(name) ?? null;
6176
+ const sleepConfig = sleepManager?.getSleepConfig(name) ?? null;
6177
+ const schedules = readSchedules(dir);
6178
+ const now = /* @__PURE__ */ new Date();
6179
+ const { upcoming, previous } = computeClockEvents(schedules, sleepState, sleepConfig, now);
6072
6180
  return c.json({ sleep: sleepState, sleepConfig, schedules, upcoming, previous });
6073
6181
  }).get("/:name/sleep/config", requireSelf(), async (c) => {
6074
6182
  const name = c.req.param("name");
@@ -6235,7 +6343,7 @@ var app15 = new Hono18().get("/:name/clock/status", requireSelf(), async (c) =>
6235
6343
  const body = await c.req.text();
6236
6344
  const message = `[webhook: ${event}] ${body}`;
6237
6345
  try {
6238
- const { sendSystemMessage } = await import("./system-chat-E65T2HSW.js");
6346
+ const { sendSystemMessage } = await import("./system-chat-NDAW6YP7.js");
6239
6347
  await sendSystemMessage(name, message);
6240
6348
  return c.json({ ok: true });
6241
6349
  } catch (err) {
@@ -6289,7 +6397,7 @@ setup.get("/status", async (c) => {
6289
6397
  let hasAccount = false;
6290
6398
  if (hasSystem) {
6291
6399
  try {
6292
- const { listUsersByType: listUsersByType2 } = await import("./auth-FRZJVQCS.js");
6400
+ const { listUsersByType: listUsersByType2 } = await import("./auth-CCFAU4GQ.js");
6293
6401
  const brains = await listUsersByType2("brain");
6294
6402
  hasAccount = brains.length > 0;
6295
6403
  } catch (err) {
@@ -6500,7 +6608,7 @@ setup.post("/account", async (c) => {
6500
6608
  }
6501
6609
  }
6502
6610
  try {
6503
- const { createUser: createUser2, updateUserProfile: updateUserProfile2 } = await import("./auth-FRZJVQCS.js");
6611
+ const { createUser: createUser2, updateUserProfile: updateUserProfile2 } = await import("./auth-CCFAU4GQ.js");
6504
6612
  const user = await createUser2(body.username.trim(), body.password);
6505
6613
  if (body.displayName?.trim()) {
6506
6614
  await updateUserProfile2(user.id, { display_name: body.displayName.trim() });
@@ -6546,7 +6654,7 @@ setup.post("/models", async (c) => {
6546
6654
  return c.json({ error: "Spirit model is required" }, 400);
6547
6655
  }
6548
6656
  try {
6549
- const { setEnabledModels: setEnabledModels3, setUtilityModel: setUtilityModel2 } = await import("./ai-service-534WUSDL.js");
6657
+ const { setEnabledModels: setEnabledModels3, setUtilityModel: setUtilityModel2 } = await import("./ai-service-UODCCE4O.js");
6550
6658
  setEnabledModels3(body.models);
6551
6659
  const config2 = readGlobalConfig();
6552
6660
  config2.spiritModel = body.spiritModel.trim();
@@ -6564,15 +6672,15 @@ setup.post("/complete", async (c) => {
6564
6672
  return c.json({ error: "Setup already complete" }, 400);
6565
6673
  }
6566
6674
  try {
6567
- const { ensureSpiritProject, syncSpiritTemplate } = await import("./spirit-II3HOERM.js");
6568
- const { startSpiritFull } = await import("./mind-service-BF2LUQMH.js");
6675
+ const { ensureSpiritProject, syncSpiritTemplate } = await import("./spirit-RPBLLUQ5.js");
6676
+ const { startSpiritFull } = await import("./mind-service-NB2JMWQZ.js");
6569
6677
  await ensureSpiritProject();
6570
6678
  await syncSpiritTemplate();
6571
6679
  const warnings = [];
6572
6680
  const config2 = readGlobalConfig();
6573
6681
  if (config2.setup?.service && config2.setup?.type === "local") {
6574
6682
  try {
6575
- const { installUserService } = await import("./service-install-4QNUDKLM.js");
6683
+ const { installUserService } = await import("./service-install-4HHFDDWY.js");
6576
6684
  const installed = await installUserService(config2.port, config2.hostname);
6577
6685
  if (!installed) {
6578
6686
  warnings.push("Service installation is not supported on this platform.");
@@ -6600,8 +6708,8 @@ setup.post("/complete", async (c) => {
6600
6708
  }
6601
6709
  let spiritConversationId;
6602
6710
  try {
6603
- const { getOrCreateMindUser: getOrCreateMindUser2, listUsersByType: listUsersByType2 } = await import("./auth-FRZJVQCS.js");
6604
- const { createConversation: createConversation2, findDMConversation: findDMConversation2 } = await import("./conversations-G4LYMHAA.js");
6711
+ const { getOrCreateMindUser: getOrCreateMindUser2, listUsersByType: listUsersByType2 } = await import("./auth-CCFAU4GQ.js");
6712
+ const { createConversation: createConversation2, findDMConversation: findDMConversation2 } = await import("./conversations-CJOOCGNW.js");
6605
6713
  const spiritUser = await getOrCreateMindUser2("volute");
6606
6714
  const brains = await listUsersByType2("brain");
6607
6715
  const admin2 = brains.find((u) => u.role === "admin");
@@ -6623,21 +6731,23 @@ setup.post("/complete", async (c) => {
6623
6731
  logger_default.info("setup complete state", { spiritConversationId, spiritStarted });
6624
6732
  if (spiritConversationId && spiritStarted) {
6625
6733
  try {
6626
- const { deliverMessage: deliverMessage2 } = await import("./message-delivery-IUAXRPFE.js");
6627
- const { listUsersByType: listUsers2 } = await import("./auth-FRZJVQCS.js");
6734
+ const { deliverMessage: deliverMessage2 } = await import("./message-delivery-R7JSQGYX.js");
6735
+ const { listUsersByType: listUsers2 } = await import("./auth-CCFAU4GQ.js");
6628
6736
  const admins = await listUsers2("brain");
6629
6737
  const admin2 = admins.find((u) => u.role === "admin");
6630
- const adminName = admin2?.display_name || admin2?.username || "the admin";
6631
- const welcomePrompt = `Setup is complete. ${adminName} just finished setting up this system and is now in your chat. Welcome them and help them create their first mind. Keep it brief \u2014 a short greeting, then ask what kind of being they'd like to bring into the world. Remember: minds are beings with their own experience, not tools \u2014 ask about personality and character, never about tasks or utility.`;
6632
- await deliverMessage2("volute", {
6633
- content: [{ type: "text", text: welcomePrompt }],
6634
- channel: `@${admin2?.username ?? "system"}`,
6635
- conversationId: spiritConversationId,
6636
- sender: admin2?.username ?? "system",
6637
- isDM: true,
6638
- participants: ["volute", admin2?.username ?? "system"],
6639
- participantCount: 2
6640
- });
6738
+ if (admin2) {
6739
+ const adminName = admin2.display_name || admin2.username;
6740
+ const welcomePrompt = `Setup is complete. ${adminName} just finished setting up this system and is now in your chat. Welcome them and help them create their first mind. Keep it brief \u2014 a short greeting, then ask what kind of being they'd like to bring into the world. Remember: minds are beings with their own experience, not tools \u2014 ask about personality and character, never about tasks or utility.`;
6741
+ await deliverMessage2(SPIRIT_NAME, {
6742
+ content: [{ type: "text", text: welcomePrompt }],
6743
+ channel: `@${admin2.username}`,
6744
+ conversationId: spiritConversationId,
6745
+ sender: admin2.username,
6746
+ isDM: true,
6747
+ participants: [SPIRIT_NAME, admin2.username],
6748
+ participantCount: 2
6749
+ });
6750
+ }
6641
6751
  } catch (err) {
6642
6752
  logger_default.warn("failed to send welcome prompt to spirit (non-fatal)", logger_default.errorData(err));
6643
6753
  warnings.push(
@@ -6734,10 +6844,10 @@ var app16 = new Hono20().get("/", async (c) => {
6734
6844
  if (!file.name.endsWith(".zip")) {
6735
6845
  return c.json({ error: "Only .zip files are accepted" }, 400);
6736
6846
  }
6737
- const buffer2 = Buffer.from(await file.arrayBuffer());
6847
+ const buffer = Buffer.from(await file.arrayBuffer());
6738
6848
  const tmpDir = mkdtempSync(join(tmpdir(), "volute-skill-upload-"));
6739
6849
  try {
6740
- const zip = new AdmZip(buffer2);
6850
+ const zip = new AdmZip(buffer);
6741
6851
  for (const entry of zip.getEntries()) {
6742
6852
  const target = resolve12(tmpDir, entry.entryName);
6743
6853
  if (!target.startsWith(tmpDir)) {
@@ -6807,7 +6917,7 @@ var app17 = new Hono21().post("/:name/typing", zValidator8("json", typingSchema)
6807
6917
  map.delete(channel, sender);
6808
6918
  }
6809
6919
  if (isConversationId(channel)) {
6810
- publish(channel, { type: "typing", senders: map.get(channel) });
6920
+ publish2(channel, { type: "typing", senders: map.get(channel) });
6811
6921
  }
6812
6922
  return c.json({ ok: true });
6813
6923
  }).get("/:name/typing", (c) => {
@@ -6970,42 +7080,6 @@ function getOnlineBrains() {
6970
7080
  return [...connections.keys()];
6971
7081
  }
6972
7082
 
6973
- // packages/daemon/src/lib/events/event-sequencer.ts
6974
- var BUFFER_SIZE = 1e3;
6975
- var MAX_AGE_MS = 5 * 60 * 1e3;
6976
- var nextId = 1;
6977
- var buffer = [];
6978
- function bufferEvent(data) {
6979
- const id = nextId++;
6980
- buffer.push({ id, data, timestamp: Date.now() });
6981
- while (buffer.length > BUFFER_SIZE) {
6982
- buffer.shift();
6983
- }
6984
- return id;
6985
- }
6986
- function nextEventId() {
6987
- return nextId++;
6988
- }
6989
- function getEventsSince(sinceId, allowedConversationIds, allowedActivityMind) {
6990
- const now = Date.now();
6991
- const startIdx = buffer.findIndex((e) => e.id > sinceId);
6992
- if (startIdx === -1) return [];
6993
- return buffer.slice(startIdx).filter((e) => {
6994
- if (now - e.timestamp >= MAX_AGE_MS) return false;
6995
- const data = e.data;
6996
- switch (data.event) {
6997
- case "snapshot":
6998
- return false;
6999
- case "conversation":
7000
- return allowedConversationIds.has(data.conversationId);
7001
- case "activity":
7002
- return allowedActivityMind === void 0 || data.mind === allowedActivityMind;
7003
- default:
7004
- return false;
7005
- }
7006
- });
7007
- }
7008
-
7009
7083
  // packages/daemon/src/web/api/v1/events.ts
7010
7084
  var app20 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
7011
7085
  const user = c.get("user");
@@ -7067,15 +7141,15 @@ var app20 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
7067
7141
  data: JSON.stringify(snapshotData)
7068
7142
  });
7069
7143
  const unsubActivity = subscribe((event) => {
7144
+ if (activityMind !== void 0 && event.mind !== activityMind) return;
7145
+ const { seq, ...rest } = event;
7070
7146
  const data = {
7071
7147
  event: "activity",
7072
- ...event,
7073
- metadata: event.metadata ?? null
7148
+ ...rest,
7149
+ metadata: rest.metadata ?? null
7074
7150
  };
7075
- const eventId = bufferEvent(data);
7076
- if (activityMind !== void 0 && event.mind !== activityMind) return;
7077
7151
  stream.writeSSE({
7078
- id: String(eventId),
7152
+ id: String(seq),
7079
7153
  data: JSON.stringify(data)
7080
7154
  }).catch((err) => {
7081
7155
  if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
@@ -7084,10 +7158,10 @@ var app20 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
7084
7158
  cleanups.push(unsubActivity);
7085
7159
  for (const conv of conversations2) {
7086
7160
  const unsubConv = subscribe2(conv.id, (event) => {
7087
- const data = { event: "conversation", conversationId: conv.id, ...event };
7088
- const eventId = bufferEvent(data);
7161
+ const { seq, ...rest } = event;
7162
+ const data = { event: "conversation", conversationId: conv.id, ...rest };
7089
7163
  stream.writeSSE({
7090
- id: String(eventId),
7164
+ id: String(seq),
7091
7165
  data: JSON.stringify(data)
7092
7166
  }).catch((err) => {
7093
7167
  if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
@@ -7125,12 +7199,9 @@ import { Hono as Hono25 } from "hono";
7125
7199
  import { spawn as spawn4 } from "child_process";
7126
7200
  import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync9 } from "fs";
7127
7201
  import { resolve as resolve13 } from "path";
7128
- function tsxBin(cwd) {
7129
- return resolve13(cwd, "node_modules", ".bin", "tsx");
7130
- }
7131
7202
  async function spawnServer(cwd, port, options) {
7132
- let cmd = tsxBin(cwd);
7133
- let args = ["src/server.ts", "--port", String(port)];
7203
+ let cmd = process.env.VOLUTE_NODE_PATH ?? process.execPath;
7204
+ let args = ["--import", "tsx", "src/server.ts", "--port", String(port)];
7134
7205
  if (options?.mindName) {
7135
7206
  if (isIsolationEnabled()) {
7136
7207
  [cmd, args] = await wrapForIsolation(cmd, args, options.mindName);
@@ -7274,6 +7345,11 @@ var app21 = new Hono25().get("/:name/variants", async (c) => {
7274
7345
  if (!entry) return c.json({ error: "Mind not found" }, 404);
7275
7346
  if (entry.stage === "seed")
7276
7347
  return c.json({ error: "Seed minds cannot create variants \u2014 sprout first" }, 403);
7348
+ if (entry.parent)
7349
+ return c.json(
7350
+ { error: "Cannot split from a variant \u2014 split from the base mind instead" },
7351
+ 403
7352
+ );
7277
7353
  let body;
7278
7354
  try {
7279
7355
  body = await c.req.json();
@@ -7287,7 +7363,7 @@ var app21 = new Hono25().get("/:name/variants", async (c) => {
7287
7363
  if (await findMind(variantName)) {
7288
7364
  return c.json({ error: `Name already in use: ${variantName}` }, 409);
7289
7365
  }
7290
- const projectRoot = mindDir(mindName);
7366
+ const projectRoot = entry.dir ?? mindDir(mindName);
7291
7367
  const variantDir = resolve14(projectRoot, ".variants", variantName);
7292
7368
  if (existsSync9(variantDir)) {
7293
7369
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
@@ -7351,7 +7427,7 @@ var app21 = new Hono25().get("/:name/variants", async (c) => {
7351
7427
  body = await c.req.json();
7352
7428
  } catch {
7353
7429
  }
7354
- const projectRoot = mindDir(mindName);
7430
+ const projectRoot = parentEntry.dir ?? mindDir(mindName);
7355
7431
  if (existsSync9(variantEntry.dir)) {
7356
7432
  const status = (await gitExec(["status", "--porcelain"], { cwd: variantEntry.dir })).trim();
7357
7433
  if (status) {
@@ -7384,7 +7460,7 @@ var app21 = new Hono25().get("/:name/variants", async (c) => {
7384
7460
  }
7385
7461
  const verified = await verify2(result.actualPort);
7386
7462
  try {
7387
- process.kill(result.child.pid);
7463
+ process.kill(-result.child.pid, "SIGTERM");
7388
7464
  } catch {
7389
7465
  }
7390
7466
  if (!verified) {
@@ -7411,13 +7487,47 @@ var app21 = new Hono25().get("/:name/variants", async (c) => {
7411
7487
  try {
7412
7488
  await gitExec(["merge", variantEntry.branch], { cwd: projectRoot });
7413
7489
  } catch (_e) {
7414
- return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
7490
+ let conflicts = [];
7491
+ try {
7492
+ const out = (await gitExec(["diff", "--name-only", "--diff-filter=U"], { cwd: projectRoot })).trim();
7493
+ conflicts = out ? out.split("\n") : [];
7494
+ } catch (err) {
7495
+ logger_default.warn(`failed to list merge conflicts for ${mindName}`, logger_default.errorData(err));
7496
+ }
7497
+ try {
7498
+ await gitExec(["merge", "--abort"], { cwd: projectRoot });
7499
+ } catch (err) {
7500
+ logger_default.warn(`failed to abort merge for ${mindName}`, logger_default.errorData(err));
7501
+ }
7502
+ try {
7503
+ await chownMindDir(projectRoot, mindName);
7504
+ } catch (err) {
7505
+ logger_default.warn(
7506
+ `failed to restore ownership after aborted merge for ${mindName}`,
7507
+ logger_default.errorData(err)
7508
+ );
7509
+ return c.json(
7510
+ {
7511
+ error: `Merge failed and restoring parent ownership failed: ${err instanceof Error ? err.message : String(err)}`,
7512
+ conflicts
7513
+ },
7514
+ 500
7515
+ );
7516
+ }
7517
+ return c.json(
7518
+ {
7519
+ error: "Merge failed. Resolve conflicts in the variant and retry the join.",
7520
+ conflicts
7521
+ },
7522
+ 500
7523
+ );
7415
7524
  }
7416
- await cleanupVariant(variantName, projectRoot, variantEntry.dir);
7525
+ await cleanupVariant(variantName, projectRoot, variantEntry.dir, { stop: true });
7526
+ await chownMindDir(projectRoot, mindName);
7417
7527
  if (variantName.endsWith("-upgrade") || variantName === "upgrade") {
7418
7528
  try {
7419
7529
  const { computeTemplateHash: computeTemplateHash2 } = await import("./template-hash-446HHH5V.js");
7420
- const { setMindTemplateHash: setMindTemplateHash2 } = await import("./registry-NUJAGLQJ.js");
7530
+ const { setMindTemplateHash: setMindTemplateHash2 } = await import("./registry-OUZBZ3Q4.js");
7421
7531
  const tmpl = parentEntry.template ?? "claude";
7422
7532
  await setMindTemplateHash2(mindName, computeTemplateHash2(tmpl));
7423
7533
  } catch (err) {
@@ -7468,7 +7578,7 @@ var app21 = new Hono25().get("/:name/variants", async (c) => {
7468
7578
  return c.json({ error: `Unknown variant: ${variantName}` }, 404);
7469
7579
  }
7470
7580
  if (!variantEntry.dir) return c.json({ error: `Variant ${variantName} has no directory` }, 500);
7471
- const projectRoot = mindDir(mindName);
7581
+ const projectRoot = parentEntry.dir ?? mindDir(mindName);
7472
7582
  await cleanupVariant(variantName, projectRoot, variantEntry.dir, { stop: true });
7473
7583
  return c.json({ ok: true });
7474
7584
  });
@@ -7648,7 +7758,7 @@ var unifiedChatApp = new Hono27().post(
7648
7758
  return c.json({ error: "conversationId or targetMind required" }, 400);
7649
7759
  }
7650
7760
  const senderName = user.id === 0 && body.sender ? body.sender : user.username;
7651
- const senderIsMind = user.id === 0 && body.sender && await findMind(body.sender) || user.user_type === "mind";
7761
+ const senderIsMind = user.id === 0 && body.sender && await findMind(body.sender) || user.user_type === "mind" || user.user_type === "system" && !!await findMind(user.username);
7652
7762
  let baseName;
7653
7763
  let variantName;
7654
7764
  let conversationId = body.conversationId;
@@ -7725,7 +7835,7 @@ var unifiedChatApp = new Hono27().post(
7725
7835
  }
7726
7836
  }
7727
7837
  if (senderIsMind) {
7728
- const vCfg = readVoluteConfig(mindDir(baseName ?? senderName));
7838
+ const vCfg = readVoluteConfig(await resolveMindDir(baseName ?? senderName));
7729
7839
  const shouldUnescapeNewlines = vCfg?.unescapeNewlines === true;
7730
7840
  for (const block of contentBlocks) {
7731
7841
  if (block.type === "text") {
@@ -7794,7 +7904,7 @@ var unifiedChatApp = new Hono27().post(
7794
7904
  });
7795
7905
  if (outboundTurnId) {
7796
7906
  const mindKey = senderBase ?? senderName;
7797
- publish2(mindKey, {
7907
+ publish3(mindKey, {
7798
7908
  mind: mindKey,
7799
7909
  type: "outbound",
7800
7910
  channel,
@@ -7818,13 +7928,16 @@ var unifiedChatApp = new Hono27().post(
7818
7928
  targetName: baseName ? (username) => username === baseName ? variantName : username : void 0
7819
7929
  });
7820
7930
  const systemReplyTarget = baseName ?? senderName;
7821
- if (senderIsMind && body.message) {
7822
- const hasSystemUser = participants.some((p) => p.userType === "system");
7823
- if (hasSystemUser) {
7824
- generateSystemReply(conversationId, systemReplyTarget, body.message).catch(
7825
- (err) => logger_default.error(`system reply generation failed for ${systemReplyTarget}`, logger_default.errorData(err))
7826
- );
7827
- }
7931
+ if (shouldGenerateSystemFallback({
7932
+ senderIsMind: !!senderIsMind,
7933
+ hasMessage: !!body.message,
7934
+ convType: conv.type,
7935
+ participants,
7936
+ replyTarget: systemReplyTarget
7937
+ })) {
7938
+ generateSystemFallbackReply(conversationId, systemReplyTarget, body.message).catch(
7939
+ (err) => logger_default.error(`system reply generation failed for ${systemReplyTarget}`, logger_default.errorData(err))
7940
+ );
7828
7941
  }
7829
7942
  return c.json({ ok: true, conversationId, outboundId });
7830
7943
  }
@@ -7917,6 +8030,9 @@ var app24 = new Hono28().get("/:name/conversations", async (c) => {
7917
8030
  if (!u) return c.json({ error: `User ${id} not found` }, 400);
7918
8031
  }
7919
8032
  const participantIds = [...participantSet];
8033
+ if (participantIds.length < 2) {
8034
+ return c.json({ error: "a conversation needs at least two distinct participants" }, 400);
8035
+ }
7920
8036
  if (participantIds.length > 2) {
7921
8037
  return c.json({ error: "Use channels for multi-participant conversations" }, 400);
7922
8038
  }
@@ -8200,27 +8316,27 @@ async function startDaemon(opts) {
8200
8316
  const DAEMON_TOKEN_PATH = resolve16(systemDir, "daemon-token");
8201
8317
  mkdirSync10(home, { recursive: true });
8202
8318
  ensureSystemDir();
8203
- const { migrateConfigSecrets, migrateSetupCompleted } = await import("./setup-73YBJZQL.js");
8319
+ const { migrateConfigSecrets, migrateSetupCompleted } = await import("./setup-Y2EJI3GG.js");
8204
8320
  migrateConfigSecrets();
8205
8321
  migrateSetupCompleted();
8206
- const { migrateAiModelQualification } = await import("./ai-service-534WUSDL.js");
8322
+ const { migrateAiModelQualification } = await import("./ai-service-UODCCE4O.js");
8207
8323
  migrateAiModelQualification();
8208
- await (await import("./db-JFLCJCUG.js")).getDb();
8324
+ await (await import("./db-MF2JNRUW.js")).getDb();
8209
8325
  try {
8210
8326
  const { eq: eq8, and: and6 } = await import("drizzle-orm");
8211
8327
  const { users: users2 } = await import("./schema-PVNG3PQM.js");
8212
- const db = await (await import("./db-JFLCJCUG.js")).getDb();
8328
+ const db = await (await import("./db-MF2JNRUW.js")).getDb();
8213
8329
  await db.update(users2).set({ role: "system" }).where(and6(eq8(users2.user_type, "system"), eq8(users2.role, "user")));
8214
8330
  } catch (err) {
8215
8331
  logger_default.warn("failed to migrate system user role", logger_default.errorData(err));
8216
8332
  }
8217
8333
  try {
8218
- const { migrateAvatarSizes } = await import("./avatar-image-BNWEXSA2.js");
8334
+ const { migrateAvatarSizes } = await import("./avatar-image-6KY6UU4D.js");
8219
8335
  await migrateAvatarSizes();
8220
8336
  } catch (err) {
8221
8337
  logger_default.warn("avatar size migration failed", logger_default.errorData(err));
8222
8338
  }
8223
- const { initSandbox } = await import("./sandbox-72MWV4JL.js");
8339
+ const { initSandbox } = await import("./sandbox-ZJ7QA6Z6.js");
8224
8340
  await initSandbox();
8225
8341
  try {
8226
8342
  await syncBuiltinSkills();
@@ -8247,7 +8363,7 @@ async function startDaemon(opts) {
8247
8363
  logger_default.warn("failed to ensure #system channel", logger_default.errorData(err));
8248
8364
  }
8249
8365
  try {
8250
- const { getOrCreateSystemUser: getOrCreateSystemUser2 } = await import("./auth-FRZJVQCS.js");
8366
+ const { getOrCreateSystemUser: getOrCreateSystemUser2 } = await import("./auth-CCFAU4GQ.js");
8251
8367
  await getOrCreateSystemUser2();
8252
8368
  } catch (err) {
8253
8369
  logger_default.warn(
@@ -8256,12 +8372,12 @@ async function startDaemon(opts) {
8256
8372
  );
8257
8373
  }
8258
8374
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes3(32).toString("hex");
8259
- const { readGlobalConfig: readGlobalConfig2 } = await import("./setup-73YBJZQL.js");
8375
+ const { readGlobalConfig: readGlobalConfig2 } = await import("./setup-Y2EJI3GG.js");
8260
8376
  const globalCfg = readGlobalConfig2();
8261
8377
  let tls;
8262
8378
  if (opts.tailscale || globalCfg.tailscale) {
8263
8379
  try {
8264
- const { getTailscaleTls } = await import("./tailscale-QAWVKQKU.js");
8380
+ const { getTailscaleTls } = await import("./tailscale-3XOKOW7T.js");
8265
8381
  const tlsConfig = await getTailscaleTls();
8266
8382
  tls = { key: tlsConfig.key, cert: tlsConfig.cert };
8267
8383
  logger_default.info("Tailscale HTTPS enabled", { hostname: tlsConfig.hostname });
@@ -8341,15 +8457,19 @@ async function startDaemon(opts) {
8341
8457
  await Promise.all(workers);
8342
8458
  }
8343
8459
  try {
8344
- const { isSetupComplete: isSetupComplete2 } = await import("./setup-73YBJZQL.js");
8460
+ const { isSetupComplete: isSetupComplete2 } = await import("./setup-Y2EJI3GG.js");
8345
8461
  if (isSetupComplete2()) {
8346
- const { ensureSpiritProject, syncSpiritTemplate } = await import("./spirit-II3HOERM.js");
8347
- const { startSpiritFull } = await import("./mind-service-BF2LUQMH.js");
8462
+ const { ensureSpiritProject, syncSpiritTemplate } = await import("./spirit-RPBLLUQ5.js");
8463
+ const { startSpiritFull } = await import("./mind-service-NB2JMWQZ.js");
8348
8464
  await ensureSpiritProject();
8465
+ const spiritEntry = await findMind(SPIRIT_NAME);
8466
+ if (spiritEntry?.dir) {
8467
+ const { registerMindDir } = await import("./delivery-router-N4A7ZC5T.js");
8468
+ registerMindDir(SPIRIT_NAME, spiritEntry.dir);
8469
+ }
8349
8470
  await syncSpiritTemplate();
8350
- const spiritEntry = await findMind("volute");
8351
- if (spiritEntry && !manager.isRunning("volute")) {
8352
- await startSpiritFull("volute");
8471
+ if (spiritEntry && !manager.isRunning(SPIRIT_NAME)) {
8472
+ await startSpiritFull(SPIRIT_NAME);
8353
8473
  }
8354
8474
  }
8355
8475
  } catch (err) {
@@ -8361,7 +8481,7 @@ async function startDaemon(opts) {
8361
8481
  bridgeManager.startBridges(daemonPort).catch((err) => {
8362
8482
  logger_default.warn("failed to start bridges", logger_default.errorData(err));
8363
8483
  });
8364
- import("./cloud-sync-Y7LQPRE3.js").then(
8484
+ import("./cloud-sync-46RS4SDA.js").then(
8365
8485
  ({ consumeQueuedMessages }) => consumeQueuedMessages().catch((err) => {
8366
8486
  logger_default.warn("failed to consume queued cloud messages", logger_default.errorData(err));
8367
8487
  })
@@ -8369,7 +8489,7 @@ async function startDaemon(opts) {
8369
8489
  logger_default.warn("failed to load cloud-sync module", logger_default.errorData(err));
8370
8490
  });
8371
8491
  try {
8372
- const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-CODJOUW2.js");
8492
+ const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-C5CH7CVP.js");
8373
8493
  backfillTemplateHashes();
8374
8494
  notifyVersionUpdate().catch((err) => {
8375
8495
  logger_default.warn("failed to send version update notifications", logger_default.errorData(err));
@@ -8387,6 +8507,7 @@ async function startDaemon(opts) {
8387
8507
  cleanExpiredLogs().catch((err) => {
8388
8508
  logger_default.warn("failed to clean expired logs", logger_default.errorData(err));
8389
8509
  });
8510
+ const maintenanceInterval = startMaintenanceInterval();
8390
8511
  setProviderRefreshHook((provider) => {
8391
8512
  void syncProviderToMinds(provider, {
8392
8513
  listRunning: () => getMindManager().getRunningMinds()
@@ -8439,6 +8560,7 @@ async function startDaemon(opts) {
8439
8560
  safe("summarizer.stop", () => summarizer.stop());
8440
8561
  safe("backupManager.stop", () => backupManager2.stop());
8441
8562
  safe("stopApiKeyRefresh", stopApiKeyRefresh);
8563
+ safe("maintenanceInterval", () => clearInterval(maintenanceInterval));
8442
8564
  safe("delivery.dispose", () => delivery.dispose());
8443
8565
  await safe("bridgeManager.stopAll", () => bridgeManager.stopAll());
8444
8566
  await safe("manager.stopAll", () => manager.stopAll());