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
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadMergedEnv
4
- } from "./chunk-5GT7URXJ.js";
4
+ } from "./chunk-ZDZISKCL.js";
5
5
  import {
6
6
  spiritDir
7
- } from "./chunk-5AUKCNPE.js";
7
+ } from "./chunk-BJZ4PV7R.js";
8
8
  import {
9
9
  hashSkillDir,
10
10
  importSkillFromDir,
11
11
  removeSharedSkill,
12
12
  sharedSkillsDir
13
- } from "./chunk-Y6OPHHGM.js";
13
+ } from "./chunk-KGIXX4NM.js";
14
14
  import {
15
15
  addMessage,
16
16
  createChannel,
@@ -20,74 +20,79 @@ import {
20
20
  getParticipants,
21
21
  joinChannel,
22
22
  publish as publish2
23
- } from "./chunk-JSRMEY52.js";
23
+ } from "./chunk-5ZQZDDIK.js";
24
24
  import {
25
25
  readVoluteConfig,
26
+ resolveWakeTriggers,
26
27
  writeVoluteConfig
27
- } from "./chunk-CU6OFXMM.js";
28
+ } from "./chunk-PFZBNKUH.js";
28
29
  import {
29
30
  isSandboxEnabled,
30
31
  wrapForSandbox
31
- } from "./chunk-OXXTPILE.js";
32
+ } from "./chunk-JZ62SYWR.js";
32
33
  import {
33
34
  markIdle,
34
35
  onMindEvent
35
- } from "./chunk-HVU45COX.js";
36
+ } from "./chunk-KQSFTPIM.js";
36
37
  import {
37
38
  extractTextContent,
38
39
  getRoutingConfig,
39
40
  resolveDeliveryMode,
40
41
  resolveRoute,
41
42
  setRoutesChangeListener
42
- } from "./chunk-ZIR7HSAX.js";
43
+ } from "./chunk-4JOQZPCE.js";
43
44
  import {
44
45
  clearMind,
45
- getPrompt,
46
46
  linkInboundToActiveTurn,
47
47
  publish as publish3,
48
48
  summarizeOrphanedTurns
49
- } from "./chunk-V4GYPD5L.js";
49
+ } from "./chunk-YXSUMLTC.js";
50
+ import {
51
+ getPrompt
52
+ } from "./chunk-4YU7HF4L.js";
50
53
  import {
51
54
  chownMindDir,
52
55
  exec,
53
56
  isIsolationEnabled,
54
57
  mindUserName,
55
58
  wrapForIsolation
56
- } from "./chunk-3ERIL673.js";
59
+ } from "./chunk-H2X54EHC.js";
57
60
  import {
58
61
  aiCompleteUtility,
59
62
  getAiConfig,
60
63
  resolveApiKey
61
- } from "./chunk-TSK5PI4L.js";
64
+ } from "./chunk-IHKGWTGT.js";
62
65
  import {
63
66
  readGlobalConfig,
64
67
  writeGlobalConfig
65
- } from "./chunk-322KE457.js";
68
+ } from "./chunk-F7H6ATM4.js";
66
69
  import {
67
70
  getOrCreateMindUser,
68
71
  getOrCreateSystemUser,
69
72
  getUser,
70
73
  getUserByUsername,
71
74
  syncMindProfile
72
- } from "./chunk-CYI534DJ.js";
75
+ } from "./chunk-OV7ZQ2GH.js";
73
76
  import {
74
77
  publish,
75
78
  subscribe
76
- } from "./chunk-ZMIMSIIT.js";
79
+ } from "./chunk-ZNMCCHCP.js";
77
80
  import {
78
81
  logger_default
79
82
  } from "./chunk-T2TP6ZC6.js";
80
83
  import {
84
+ SPIRIT_NAME,
81
85
  findMind,
82
86
  getBaseName,
83
87
  getDb,
88
+ isSpiritName,
84
89
  mindDir,
85
90
  mindTmpDir,
86
91
  setMindRunning,
87
92
  stateDir,
88
93
  voluteHome,
89
94
  voluteSystemDir
90
- } from "./chunk-H7IYFRAL.js";
95
+ } from "./chunk-ALKYQULU.js";
91
96
  import {
92
97
  activity,
93
98
  deliveryQueue,
@@ -335,6 +340,9 @@ function revokeMindToken(mindName) {
335
340
  function resolveMindToken(token) {
336
341
  return tokenToMind.get(token) ?? null;
337
342
  }
343
+ function getMindToken(mindName) {
344
+ return mindToToken.get(mindName) ?? null;
345
+ }
338
346
 
339
347
  // packages/daemon/src/lib/daemon/notices.ts
340
348
  import { and, asc, eq, lte, or, sql } from "drizzle-orm";
@@ -763,21 +771,8 @@ var MindManager = class {
763
771
  if (isIsolationEnabled()) {
764
772
  env.HOME = resolve2(dir, "home");
765
773
  }
766
- const customNode = process.env.VOLUTE_NODE_PATH;
767
- let baseBin;
768
- let baseArgs;
769
- if (customNode) {
770
- baseBin = customNode;
771
- baseArgs = [
772
- resolve2(dir, "node_modules", ".bin", "tsx"),
773
- "src/server.ts",
774
- "--port",
775
- String(port)
776
- ];
777
- } else {
778
- baseBin = resolve2(dir, "node_modules", ".bin", "tsx");
779
- baseArgs = ["src/server.ts", "--port", String(port)];
780
- }
774
+ const baseBin = process.env.VOLUTE_NODE_PATH ?? process.execPath;
775
+ const baseArgs = ["--import", "tsx", "src/server.ts", "--port", String(port)];
781
776
  let spawnCmd;
782
777
  let spawnArgs;
783
778
  if (isIsolationEnabled()) {
@@ -939,7 +934,7 @@ var MindManager = class {
939
934
  if (this.shuttingDown || this.stopping.has(name)) return;
940
935
  mlog.error(`mind ${name} exited with code ${code}`);
941
936
  try {
942
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-YAHDOIU5.js");
937
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
943
938
  const sleepMgr = getSleepManagerIfReady2();
944
939
  const sleepState = sleepMgr?.getState(name);
945
940
  if (sleepState?.sleeping) {
@@ -969,15 +964,15 @@ var MindManager = class {
969
964
  (err) => mlog.warn(`failed to clear turn state for ${name} after crash`, logger_default.errorData(err))
970
965
  );
971
966
  try {
972
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-R5STGLXB.js");
967
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-JINZRG2W.js");
973
968
  getDeliveryManager2().clearMindSessions(name);
974
969
  } catch (err) {
975
970
  if (!(err instanceof Error && err.message.includes("not initialized"))) {
976
971
  mlog.warn(`failed to clear delivery state for ${name} after crash`, logger_default.errorData(err));
977
972
  }
978
973
  }
979
- import("./mind-activity-tracker-BGHJE7XA.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
980
- import("./activity-events-OMCSM7XW.js").then(
974
+ import("./mind-activity-tracker-3QRNJVR4.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
975
+ import("./activity-events-NF6VWTJQ.js").then(
981
976
  ({ publish: publish4 }) => publish4({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
982
977
  ).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
983
978
  const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
@@ -1035,7 +1030,7 @@ var MindManager = class {
1035
1030
  mlog.warn(`failed to clear turn state for ${name} on stop`, logger_default.errorData(err));
1036
1031
  }
1037
1032
  try {
1038
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-R5STGLXB.js");
1033
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-JINZRG2W.js");
1039
1034
  getDeliveryManager2().clearMindSessions(name);
1040
1035
  } catch (err) {
1041
1036
  if (!(err instanceof Error && err.message.includes("not initialized"))) {
@@ -1043,7 +1038,7 @@ var MindManager = class {
1043
1038
  }
1044
1039
  }
1045
1040
  try {
1046
- const { clearEchoTextCache } = await import("./echo-text-NQXYYYCU.js");
1041
+ const { clearEchoTextCache } = await import("./echo-text-VQPB5L2X.js");
1047
1042
  clearEchoTextCache(name);
1048
1043
  } catch (err) {
1049
1044
  mlog.debug(`failed to clear echo-text cache for ${name}`, logger_default.errorData(err));
@@ -1220,6 +1215,10 @@ async function getSessionUserId(sessionId) {
1220
1215
  return row.userId;
1221
1216
  }
1222
1217
  async function cleanExpiredSessions() {
1218
+ const now = Date.now();
1219
+ for (const [id, cached] of sessionCache) {
1220
+ if (cached.expires <= now) sessionCache.delete(id);
1221
+ }
1223
1222
  const db = await getDb();
1224
1223
  const cutoff = Date.now() - SESSION_MAX_AGE;
1225
1224
  await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
@@ -1699,7 +1698,7 @@ function resolveSkillsDir(manifest) {
1699
1698
  async function discoverBuiltinExtensions(disabledIds) {
1700
1699
  const builtins = [
1701
1700
  { id: "notes", load: async () => (await import("./src-YH6PQ7LJ.js")).default },
1702
- { id: "pages", load: async () => (await import("./src-XFY3XDD6.js")).default },
1701
+ { id: "pages", load: async () => (await import("./src-OJHARHTF.js")).default },
1703
1702
  { id: "plan", load: async () => (await import("./src-JAOVXO5W.js")).default }
1704
1703
  ];
1705
1704
  const results = [];
@@ -1949,7 +1948,7 @@ async function installNpmExtension(pkg) {
1949
1948
  throw new Error(`Extension "${pkg}" is already installed`);
1950
1949
  }
1951
1950
  const dir = ensureExtensionsNpmDir();
1952
- const { exec: exec2 } = await import("./exec-4GWHAHS3.js");
1951
+ const { exec: exec2 } = await import("./exec-G3ZRST4Z.js");
1953
1952
  try {
1954
1953
  await exec2("npm", ["install", "--ignore-scripts", pkg], { cwd: dir });
1955
1954
  } catch (err) {
@@ -1970,7 +1969,7 @@ async function uninstallNpmExtension(pkg) {
1970
1969
  packages.splice(idx, 1);
1971
1970
  writeExtensionsConfig(packages);
1972
1971
  try {
1973
- const { exec: exec2 } = await import("./exec-4GWHAHS3.js");
1972
+ const { exec: exec2 } = await import("./exec-G3ZRST4Z.js");
1974
1973
  await exec2("npm", ["uninstall", "--ignore-scripts", pkg], { cwd: extensionsNpmDir() });
1975
1974
  } catch (err) {
1976
1975
  logger_default.warn(
@@ -2300,6 +2299,7 @@ async function ensureMailAddress(mindName) {
2300
2299
  import { resolve as resolve5 } from "path";
2301
2300
  import { CronExpressionParser } from "cron-parser";
2302
2301
  var slog2 = logger_default.child("scheduler");
2302
+ var CATCHUP_STALE_MINUTES = 10;
2303
2303
  var Scheduler = class {
2304
2304
  schedules = /* @__PURE__ */ new Map();
2305
2305
  mindDirs = /* @__PURE__ */ new Map();
@@ -2339,6 +2339,26 @@ var Scheduler = class {
2339
2339
  } else {
2340
2340
  this.schedules.delete(mindName);
2341
2341
  }
2342
+ const epochMinute = Math.floor(Date.now() / 6e4);
2343
+ const validKeys = new Set(schedules.map((s) => `${mindName}:${s.id}`));
2344
+ let changed = false;
2345
+ for (const key of this.lastFired.keys()) {
2346
+ if (key.startsWith(`${mindName}:`) && !validKeys.has(key)) {
2347
+ this.lastFired.delete(key);
2348
+ changed = true;
2349
+ }
2350
+ }
2351
+ for (const key of validKeys) {
2352
+ if (!this.lastFired.has(key)) {
2353
+ this.lastFired.set(key, epochMinute);
2354
+ changed = true;
2355
+ }
2356
+ }
2357
+ if (changed) {
2358
+ this.saveState().catch(
2359
+ (err) => slog2.warn(`failed to persist scheduler state for ${mindName}`, logger_default.errorData(err))
2360
+ );
2361
+ }
2342
2362
  }
2343
2363
  unloadSchedules(mindName) {
2344
2364
  this.schedules.delete(mindName);
@@ -2384,11 +2404,14 @@ var Scheduler = class {
2384
2404
  return false;
2385
2405
  }
2386
2406
  }
2387
- if (prevMinute === epochMinute) {
2388
- this.lastFired.set(key, epochMinute);
2389
- return true;
2407
+ const lastFired = this.lastFired.get(key) ?? epochMinute;
2408
+ if (prevMinute <= lastFired) return false;
2409
+ this.lastFired.set(key, prevMinute);
2410
+ if (epochMinute - prevMinute > CATCHUP_STALE_MINUTES) {
2411
+ slog2.info(`skipping stale catch-up fire for ${key} (${epochMinute - prevMinute}min late)`);
2412
+ return false;
2390
2413
  }
2391
- return false;
2414
+ return true;
2392
2415
  }
2393
2416
  async fire(mindName, schedule) {
2394
2417
  try {
@@ -2415,7 +2438,9 @@ ${stderr}` : ""}`;
2415
2438
  return;
2416
2439
  }
2417
2440
  await this.deliverSystem(mindName, `[${schedule.id}] ${text}`, {
2418
- whileSleeping: schedule.whileSleeping,
2441
+ // Default schedule fires to "queue" while asleep so an unadorned cron
2442
+ // schedule doesn't inherit the DM wake-trigger fallback and wake the mind.
2443
+ whileSleeping: schedule.whileSleeping ?? "queue",
2419
2444
  session: schedule.session
2420
2445
  });
2421
2446
  slog2.info(`fired "${schedule.id}" for ${mindName}`);
@@ -2452,15 +2477,41 @@ ${stderr}` : ""}`;
2452
2477
  }
2453
2478
  }
2454
2479
  async runScript(script, cwd, mindName) {
2480
+ const env = await this.buildScriptEnv(mindName);
2455
2481
  if (isSandboxEnabled()) {
2456
2482
  const dir = this.mindDirs.get(mindName) ?? mindDir(mindName);
2457
2483
  const [cmd, args] = await wrapForSandbox("bash", ["-c", script], dir, mindName, [
2458
2484
  dir,
2459
2485
  mindTmpDir(dir)
2460
2486
  ]);
2461
- return exec(cmd, args, { cwd });
2487
+ return exec(cmd, args, { cwd, env });
2462
2488
  }
2463
- return exec("bash", ["-c", script], { cwd, mindName });
2489
+ return exec("bash", ["-c", script], { cwd, mindName, env });
2490
+ }
2491
+ /**
2492
+ * Build the environment for a scheduled script — mirrors the mind process env
2493
+ * (allowlisted base + merged mind env + VOLUTE_* runtime vars), authenticated
2494
+ * with the mind's own per-mind, non-admin token. Reuses the running mind's
2495
+ * token when present; otherwise mints one (stable across fires, regenerated on
2496
+ * next mind start). The daemon admin token is never included.
2497
+ */
2498
+ async buildScriptEnv(mindName) {
2499
+ const dir = this.mindDirs.get(mindName) ?? mindDir(mindName);
2500
+ const entry = await findMind(mindName);
2501
+ const token = getMindToken(mindName) ?? generateMindToken(mindName);
2502
+ const mindLocalBin = resolve5(dir, "home", ".local", "bin");
2503
+ const currentPath = process.env.PATH ?? "";
2504
+ return {
2505
+ ...buildMindBaseEnv(),
2506
+ ...loadMergedEnv(mindName),
2507
+ VOLUTE_MIND: mindName,
2508
+ VOLUTE_STATE_DIR: stateDir(mindName),
2509
+ VOLUTE_MIND_DIR: dir,
2510
+ VOLUTE_MIND_PORT: entry ? String(entry.port) : void 0,
2511
+ VOLUTE_MIND_TOKEN: token,
2512
+ TMPDIR: mindTmpDir(dir),
2513
+ PATH: `${mindLocalBin}:${currentPath}`
2514
+ };
2464
2515
  }
2465
2516
  deliverSystem(mindName, text, opts) {
2466
2517
  return sendSystemMessage(mindName, text, opts);
@@ -2705,11 +2756,7 @@ async function startMindFull(name) {
2705
2756
  if (entry?.parent) return;
2706
2757
  if (!entry || entry.stage === "seed") {
2707
2758
  if (entry?.stage === "seed") {
2708
- const creatorNote = entry.createdBy ? ` Your creator is ${entry.createdBy}. Send them a message to introduce yourself.` : "";
2709
- sendSystemMessage(
2710
- baseName,
2711
- `You've just been created. A human planted you as a seed. Start a conversation with them \u2014 introduce yourself, ask questions, and begin exploring who you want to be.${creatorNote}`
2712
- ).catch(
2759
+ sendSeedOrientation(baseName, entry.createdBy).catch(
2713
2760
  (err) => logger_default.error(`failed to send seed orientation for ${baseName}`, logger_default.errorData(err))
2714
2761
  );
2715
2762
  if (entry.createdBy) {
@@ -2771,7 +2818,7 @@ async function wakeMind(name) {
2771
2818
  async function startSpiritFull(name) {
2772
2819
  const entry = await findMind(name);
2773
2820
  if (entry?.dir) {
2774
- const { registerMindDir } = await import("./delivery-router-6DE3BPHO.js");
2821
+ const { registerMindDir } = await import("./delivery-router-N4A7ZC5T.js");
2775
2822
  registerMindDir(name, entry.dir);
2776
2823
  }
2777
2824
  await getMindManager().startMind(name);
@@ -2792,9 +2839,23 @@ async function stopSpiritFull(name) {
2792
2839
  summary: `${name} spirit stopped`
2793
2840
  }).catch((err) => logger_default.error("failed to publish spirit_stopped activity", logger_default.errorData(err)));
2794
2841
  }
2842
+ function buildSeedOrientation(createdBy, creatorType) {
2843
+ const intro = "You've just been created as a seed. Start a conversation \u2014 introduce yourself, ask questions, and begin exploring who you want to be.";
2844
+ if (!createdBy) return intro;
2845
+ const who = creatorType === "mind" ? `another mind, ${createdBy}` : creatorType === "system" ? `the spirit of this system (${createdBy})` : createdBy;
2846
+ return `${intro} Your creator is ${who}. Send them a message to introduce yourself.`;
2847
+ }
2848
+ async function sendSeedOrientation(mindName, createdBy) {
2849
+ let creatorType;
2850
+ if (createdBy) {
2851
+ const { getUserByUsername: getUserByUsername2 } = await import("./auth-CCFAU4GQ.js");
2852
+ creatorType = (await getUserByUsername2(createdBy))?.user_type;
2853
+ }
2854
+ await sendSystemMessage(mindName, buildSeedOrientation(createdBy, creatorType));
2855
+ }
2795
2856
  async function ensureCreatorDM(mindName, creatorUsername) {
2796
- const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-FRZJVQCS.js");
2797
- const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-G4LYMHAA.js");
2857
+ const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-CCFAU4GQ.js");
2858
+ const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-CJOOCGNW.js");
2798
2859
  const mindUser = await getOrCreateMindUser2(mindName);
2799
2860
  const creatorUser = await getUserByUsername2(creatorUsername);
2800
2861
  if (!creatorUser) {
@@ -2827,6 +2888,8 @@ async function stopMindFull(name) {
2827
2888
 
2828
2889
  // packages/daemon/src/lib/daemon/sleep-manager.ts
2829
2890
  var slog3 = logger_default.child("sleep");
2891
+ var MAX_WAKE_ATTEMPTS = 5;
2892
+ var WAKE_BACKOFF_MS = [6e4, 5 * 6e4, 15 * 6e4, 60 * 6e4];
2830
2893
  function defaultState() {
2831
2894
  return {
2832
2895
  sleeping: false,
@@ -2835,7 +2898,10 @@ function defaultState() {
2835
2898
  wokenByTrigger: false,
2836
2899
  voluntaryWakeAt: null,
2837
2900
  queuedMessageCount: 0,
2838
- triggerWakeHistory: []
2901
+ triggerWakeHistory: [],
2902
+ wakeFailures: 0,
2903
+ nextWakeAttemptAt: null,
2904
+ lastWakeAt: null
2839
2905
  };
2840
2906
  }
2841
2907
  function formatCurrentDate() {
@@ -2905,6 +2971,9 @@ var SleepManager = class {
2905
2971
  const data = JSON.parse(readFileSync7(this.statePath, "utf-8"));
2906
2972
  for (const [name, state2] of Object.entries(data)) {
2907
2973
  state2.triggerWakeHistory ??= [];
2974
+ state2.wakeFailures ??= 0;
2975
+ state2.nextWakeAttemptAt ??= null;
2976
+ state2.lastWakeAt ??= null;
2908
2977
  this.states.set(name, state2);
2909
2978
  }
2910
2979
  }
@@ -2915,7 +2984,7 @@ var SleepManager = class {
2915
2984
  saveState() {
2916
2985
  const data = {};
2917
2986
  for (const [name, state2] of this.states) {
2918
- if (state2.sleeping) data[name] = state2;
2987
+ if (state2.sleeping || state2.lastWakeAt) data[name] = state2;
2919
2988
  }
2920
2989
  try {
2921
2990
  writeFileSync6(this.statePath, `${JSON.stringify(data, null, 2)}
@@ -2965,8 +3034,9 @@ var SleepManager = class {
2965
3034
  }
2966
3035
  await this.archiveSessions(name);
2967
3036
  state2.sleeping = true;
2968
- state2.sleepingSince = (/* @__PURE__ */ new Date()).toISOString();
2969
- state2.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
3037
+ if (!state2.voluntaryWakeAt) {
3038
+ state2.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
3039
+ }
2970
3040
  this.saveState();
2971
3041
  slog3.info(`${name} returned to sleep after crashing during trigger wake`);
2972
3042
  } catch (err) {
@@ -3061,12 +3131,13 @@ var SleepManager = class {
3061
3131
  try {
3062
3132
  await wakeMind(name);
3063
3133
  } catch (err) {
3064
- slog3.error(`failed to wake ${name}`, logger_default.errorData(err));
3134
+ await this.handleWakeFailure(name, err);
3065
3135
  return;
3066
3136
  }
3067
3137
  }
3068
3138
  const entry = await findMind(name);
3069
3139
  if (!entry) return;
3140
+ this.resetWakeBackoff(name);
3070
3141
  if (opts?.trigger) {
3071
3142
  state2.wokenByTrigger = true;
3072
3143
  state2.triggerWakeHistory.push({
@@ -3121,6 +3192,7 @@ var SleepManager = class {
3121
3192
  } catch (err) {
3122
3193
  slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
3123
3194
  }
3195
+ await this.waitForIdle(name, 12e4);
3124
3196
  }
3125
3197
  const flushed = await this.flushQueuedMessages(name);
3126
3198
  if (flushed > 0) {
@@ -3140,8 +3212,7 @@ var SleepManager = class {
3140
3212
  checkWakeTrigger(name, payload) {
3141
3213
  const config = this.getSleepConfig(name);
3142
3214
  const triggers = config?.wakeTriggers;
3143
- const mentionsEnabled = triggers?.mentions !== false;
3144
- const dmsEnabled = triggers?.dms !== false;
3215
+ const { mentions: mentionsEnabled, dms: dmsEnabled } = resolveWakeTriggers(triggers);
3145
3216
  if (dmsEnabled && payload.isDM) return true;
3146
3217
  if (mentionsEnabled && payload.content) {
3147
3218
  const text = typeof payload.content === "string" ? payload.content : Array.isArray(payload.content) ? payload.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join(" ") : "";
@@ -3179,53 +3250,159 @@ var SleepManager = class {
3179
3250
  }
3180
3251
  }
3181
3252
  /**
3182
- * Flush all queued sleep messages for a mind through the delivery manager.
3253
+ * Flush all queued sleep messages for a mind on wake. Rows are grouped by channel
3254
+ * and each channel group is delivered as ONE pre-batched turn (#382) — a night's
3255
+ * backlog becomes one stack per channel, not N cold-start doorbells. Delivery is
3256
+ * all-or-nothing per channel: a failed group's rows stay queued for the next wake,
3257
+ * while other channels still flush.
3183
3258
  */
3184
3259
  async flushQueuedMessages(name) {
3185
3260
  try {
3186
3261
  const db = await getDb();
3187
3262
  const rows = await db.select().from(deliveryQueue).where(and2(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
3188
3263
  if (rows.length === 0) return 0;
3189
- const { deliverMessage: deliverMessage2 } = await import("./message-delivery-IUAXRPFE.js");
3190
- const delivered = [];
3264
+ const groups = /* @__PURE__ */ new Map();
3191
3265
  for (const row of rows) {
3266
+ let payload;
3192
3267
  try {
3193
- await deliverMessage2(name, JSON.parse(row.payload));
3194
- delivered.push(row.id);
3268
+ payload = JSON.parse(row.payload);
3195
3269
  } catch (err) {
3196
- slog3.warn(`failed to flush queued message ${row.id} for ${name}`, logger_default.errorData(err));
3270
+ slog3.warn(
3271
+ `dropping unparseable queued message ${row.id} for ${name}`,
3272
+ logger_default.errorData(err)
3273
+ );
3274
+ await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
3275
+ continue;
3197
3276
  }
3277
+ const channel = payload.channel ?? "unknown";
3278
+ if (!groups.has(channel)) groups.set(channel, []);
3279
+ groups.get(channel)?.push({ id: row.id, payload });
3198
3280
  }
3199
- if (delivered.length > 0) {
3200
- await db.delete(deliveryQueue).where(inArray(deliveryQueue.id, delivered));
3281
+ let deliveredCount = 0;
3282
+ for (const [channel, items] of groups) {
3283
+ const delivered = await this.deliverQueuedBatch(
3284
+ name,
3285
+ items.map((i) => i.payload)
3286
+ );
3287
+ if (!delivered) {
3288
+ slog3.warn(
3289
+ `failed to flush ${items.length} queued message(s) on ${channel} for ${name}; leaving them queued`
3290
+ );
3291
+ continue;
3292
+ }
3293
+ await db.delete(deliveryQueue).where(
3294
+ inArray(
3295
+ deliveryQueue.id,
3296
+ items.map((i) => i.id)
3297
+ )
3298
+ );
3299
+ deliveredCount += items.length;
3201
3300
  }
3202
3301
  const state2 = this.states.get(name);
3203
3302
  if (state2) {
3204
- state2.queuedMessageCount = Math.max(0, state2.queuedMessageCount - delivered.length);
3303
+ state2.queuedMessageCount = Math.max(0, state2.queuedMessageCount - deliveredCount);
3205
3304
  }
3206
- return delivered.length;
3305
+ return deliveredCount;
3207
3306
  } catch (err) {
3208
3307
  slog3.warn(`failed to flush queued messages for ${name}`, logger_default.errorData(err));
3209
3308
  return 0;
3210
3309
  }
3211
3310
  }
3311
+ /**
3312
+ * Deliver one channel's worth of queued messages as a single pre-batched turn on
3313
+ * the wake path. Extracted as a seam so the flush loop can be unit-tested without
3314
+ * the real delivery stack. Returns whether the batch was handed off to the mind.
3315
+ */
3316
+ async deliverQueuedBatch(name, payloads) {
3317
+ const { deliverBatch: deliverBatch2 } = await import("./message-delivery-R7JSQGYX.js");
3318
+ return deliverBatch2(name, payloads);
3319
+ }
3320
+ // --- Wake failure handling (#419) ---
3321
+ /**
3322
+ * Record a failed wake attempt. Applies exponential backoff between retries and,
3323
+ * after MAX_WAKE_ATTEMPTS consecutive failures, gives up and marks the mind errored
3324
+ * rather than retrying every tick forever.
3325
+ */
3326
+ async handleWakeFailure(name, err) {
3327
+ const state2 = this.states.get(name);
3328
+ if (!state2?.sleeping) return;
3329
+ state2.wakeFailures = (state2.wakeFailures ?? 0) + 1;
3330
+ if (state2.wakeFailures >= MAX_WAKE_ATTEMPTS) {
3331
+ await this.markWakeErrored(name, err);
3332
+ return;
3333
+ }
3334
+ const backoff = WAKE_BACKOFF_MS[Math.min(state2.wakeFailures - 1, WAKE_BACKOFF_MS.length - 1)];
3335
+ state2.nextWakeAttemptAt = new Date(Date.now() + backoff).toISOString();
3336
+ this.saveState();
3337
+ slog3.warn(
3338
+ `failed to wake ${name} (attempt ${state2.wakeFailures}/${MAX_WAKE_ATTEMPTS}); retrying in ${Math.round(backoff / 6e4)}m`,
3339
+ logger_default.errorData(err)
3340
+ );
3341
+ }
3342
+ /** Clear wake-failure backoff after a successful wake. */
3343
+ resetWakeBackoff(name) {
3344
+ const state2 = this.states.get(name);
3345
+ if (!state2) return;
3346
+ if (state2.wakeFailures === 0 && state2.nextWakeAttemptAt === null) return;
3347
+ state2.wakeFailures = 0;
3348
+ state2.nextWakeAttemptAt = null;
3349
+ this.saveState();
3350
+ }
3351
+ /**
3352
+ * A mind has failed to wake too many times — it isn't asleep, it's broken.
3353
+ * Clear the sleep state (so the tick stops retrying) and surface the failure
3354
+ * loudly: error log, activity event for the web UI, and a mind notice so the
3355
+ * mind learns what happened on its next successful session. A manual
3356
+ * `volute clock wake` / `volute mind start` is the recovery path.
3357
+ */
3358
+ async markWakeErrored(name, err) {
3359
+ const detail = err instanceof Error ? err.message : String(err);
3360
+ this.states.delete(name);
3361
+ this.saveState();
3362
+ slog3.error(
3363
+ `${name} failed to wake ${MAX_WAKE_ATTEMPTS} times in a row; giving up and marking errored`,
3364
+ logger_default.errorData(err)
3365
+ );
3366
+ try {
3367
+ await publish({
3368
+ type: "mind_stopped",
3369
+ mind: name,
3370
+ summary: `${name} failed to wake after ${MAX_WAKE_ATTEMPTS} attempts`,
3371
+ metadata: { error: "wake_failed", detail }
3372
+ });
3373
+ } catch (e) {
3374
+ slog3.error(`failed to publish wake-failure activity for ${name}`, logger_default.errorData(e));
3375
+ }
3376
+ await recordNotice({
3377
+ mind: name,
3378
+ session: MIND_LEVEL_SESSION,
3379
+ kind: "startup",
3380
+ reason: "startup_failed",
3381
+ detail: `Wake failed ${MAX_WAKE_ATTEMPTS} times in a row and was given up. Last error: ${detail}`
3382
+ });
3383
+ }
3212
3384
  // --- Internal methods ---
3213
3385
  markSleeping(name, opts) {
3214
3386
  const sleepConfig = this.getSleepConfig(name);
3215
3387
  const state2 = {
3216
3388
  sleeping: true,
3217
3389
  sleepingSince: (/* @__PURE__ */ new Date()).toISOString(),
3218
- scheduledWakeAt: this.getNextWakeTime(sleepConfig),
3390
+ // An explicit voluntary wake-at is authoritative for the night — null out
3391
+ // the cron wake so an earlier scheduled wake can't silently override it.
3392
+ scheduledWakeAt: opts?.voluntaryWakeAt ? null : this.getNextWakeTime(sleepConfig),
3219
3393
  wokenByTrigger: false,
3220
3394
  voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
3221
3395
  queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
3222
- triggerWakeHistory: []
3396
+ triggerWakeHistory: [],
3397
+ wakeFailures: 0,
3398
+ nextWakeAttemptAt: null,
3399
+ lastWakeAt: this.states.get(name)?.lastWakeAt ?? null
3223
3400
  };
3224
3401
  this.states.set(name, state2);
3225
3402
  this.saveState();
3226
3403
  }
3227
3404
  markAwake(name) {
3228
- this.states.delete(name);
3405
+ this.states.set(name, { ...defaultState(), lastWakeAt: (/* @__PURE__ */ new Date()).toISOString() });
3229
3406
  this.saveState();
3230
3407
  }
3231
3408
  getNextWakeTime(config) {
@@ -3240,52 +3417,78 @@ var SleepManager = class {
3240
3417
  }
3241
3418
  async tick() {
3242
3419
  const now = /* @__PURE__ */ new Date();
3243
- const epochMinute = Math.floor(now.getTime() / 6e4);
3244
3420
  const mindNames = /* @__PURE__ */ new Set([...this.sleepConfigs.keys(), ...this.states.keys()]);
3245
3421
  for (const name of mindNames) {
3246
- if (!this.isSleeping(name)) {
3247
- const mind = await findMind(name);
3248
- if (!mind?.running) continue;
3249
- }
3250
- const config = this.getSleepConfig(name);
3251
- if (!config?.enabled || !config.schedule) continue;
3252
- const state2 = this.states.get(name);
3253
- if (state2?.sleeping && state2.voluntaryWakeAt) {
3254
- const wakeAt = new Date(state2.voluntaryWakeAt);
3255
- if (now >= wakeAt) {
3256
- this.initiateWake(name).catch(
3257
- (err) => slog3.error(`failed voluntary wake for ${name}`, logger_default.errorData(err))
3258
- );
3259
- continue;
3260
- }
3261
- }
3262
- if (state2?.sleeping && state2.scheduledWakeAt) {
3263
- const wakeAt = new Date(state2.scheduledWakeAt);
3264
- if (now >= wakeAt) {
3265
- this.initiateWake(name).catch(
3266
- (err) => slog3.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
3267
- );
3268
- continue;
3269
- }
3422
+ await this.evaluateMind(name, now);
3423
+ }
3424
+ }
3425
+ /**
3426
+ * Per-mind tick evaluation. The wake checks depend only on the persisted sleep
3427
+ * state — never on a currently-enabled sleep config — so a mind that was put to
3428
+ * sleep with a wake time still wakes even if the schedule is later disabled or
3429
+ * removed (or never existed, e.g. a bare `clock sleep --wake-at`). Only the
3430
+ * sleep-onset check requires a running mind with an enabled schedule.
3431
+ */
3432
+ async evaluateMind(name, now) {
3433
+ const state2 = this.states.get(name);
3434
+ if (state2?.sleeping) {
3435
+ if (state2.nextWakeAttemptAt && now < new Date(state2.nextWakeAttemptAt)) return;
3436
+ if (state2.voluntaryWakeAt && now >= new Date(state2.voluntaryWakeAt)) {
3437
+ this.initiateWake(name).catch(
3438
+ (err) => slog3.error(`failed voluntary wake for ${name}`, logger_default.errorData(err))
3439
+ );
3440
+ return;
3270
3441
  }
3271
- if (!state2?.sleeping) {
3272
- if (this.shouldSleep(config.schedule.sleep, epochMinute)) {
3273
- this.initiateSleep(name).catch(
3274
- (err) => slog3.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
3275
- );
3276
- }
3442
+ if (state2.scheduledWakeAt && now >= new Date(state2.scheduledWakeAt)) {
3443
+ this.initiateWake(name).catch(
3444
+ (err) => slog3.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
3445
+ );
3446
+ return;
3277
3447
  }
3448
+ return;
3449
+ }
3450
+ const mind = await findMind(name);
3451
+ if (!mind?.running) return;
3452
+ const config = this.getSleepConfig(name);
3453
+ if (!config?.enabled || !config.schedule) return;
3454
+ if (this.shouldSleepNow(config.schedule, state2?.lastWakeAt ?? null, now)) {
3455
+ this.initiateSleep(name).catch(
3456
+ (err) => slog3.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
3457
+ );
3278
3458
  }
3279
3459
  }
3280
- shouldSleep(cronExpr, epochMinute) {
3460
+ /**
3461
+ * Level-triggered sleep-onset decision: sleep whenever we're inside the nightly
3462
+ * window, not only on the exact sleep minute — so a daemon restart or a stalled
3463
+ * tick across the sleep minute still puts the mind to bed on the next tick.
3464
+ *
3465
+ * Manual-wake exemption: a full wake inside the current window (human
3466
+ * `clock wake`, or a scheduled wake) suppresses re-sleep until the next window
3467
+ * begins, so `volute clock wake` at 3am sticks instead of being undone a minute
3468
+ * later.
3469
+ */
3470
+ shouldSleepNow(schedule, lastWakeAt, now) {
3471
+ const { inWindow, windowStart } = this.inSleepWindow(schedule, now);
3472
+ if (!inWindow || !windowStart) return false;
3473
+ if (lastWakeAt && new Date(lastWakeAt) >= windowStart) return false;
3474
+ return true;
3475
+ }
3476
+ /**
3477
+ * Whether `now` falls inside the mind's nightly sleep window: the most recent
3478
+ * sleep-cron fire is more recent than the most recent wake-cron fire. Returns
3479
+ * the window start (last sleep fire) for the manual-wake exemption check.
3480
+ */
3481
+ inSleepWindow(schedule, now) {
3281
3482
  try {
3282
- const interval = CronExpressionParser2.parse(cronExpr);
3283
- const prev = interval.prev().toDate();
3284
- const prevMinute = Math.floor(prev.getTime() / 6e4);
3285
- return prevMinute === epochMinute;
3483
+ const prevSleep = CronExpressionParser2.parse(schedule.sleep, { currentDate: now }).prev().toDate();
3484
+ const prevWake = CronExpressionParser2.parse(schedule.wake, { currentDate: now }).prev().toDate();
3485
+ return { inWindow: prevSleep > prevWake, windowStart: prevSleep };
3286
3486
  } catch (err) {
3287
- slog3.warn(`invalid sleep cron "${cronExpr}"`, logger_default.errorData(err));
3288
- return false;
3487
+ slog3.warn(
3488
+ `invalid sleep/wake cron "${schedule.sleep}"/"${schedule.wake}"`,
3489
+ logger_default.errorData(err)
3490
+ );
3491
+ return { inWindow: false, windowStart: null };
3289
3492
  }
3290
3493
  }
3291
3494
  async waitForIdle(name, timeoutMs) {
@@ -3477,9 +3680,10 @@ var SleepManager = class {
3477
3680
  this.transitioning.add(event.mind);
3478
3681
  sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
3479
3682
  state2.sleeping = true;
3480
- state2.sleepingSince = (/* @__PURE__ */ new Date()).toISOString();
3481
- const sleepConfig = this.getSleepConfig(event.mind);
3482
- state2.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
3683
+ if (!state2.voluntaryWakeAt) {
3684
+ const sleepConfig = this.getSleepConfig(event.mind);
3685
+ state2.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
3686
+ }
3483
3687
  this.saveState();
3484
3688
  slog3.info(`${event.mind} returned to sleep`);
3485
3689
  }).catch((err) => {
@@ -3646,13 +3850,11 @@ async function checkStaleSend(mindBase, conversationId, excludeSenders) {
3646
3850
  unseen: unseen.map((r) => ({ sender: r.sender, text: extractText(r.content) }))
3647
3851
  };
3648
3852
  }
3853
+ function clearMind2(mindBase) {
3854
+ state.delete(mindBase);
3855
+ }
3649
3856
  function resetTurn(mindBase) {
3650
- const conv = state.get(mindBase);
3651
- if (!conv) return;
3652
- for (const entry of conv.values()) {
3653
- entry.turnOpen = false;
3654
- entry.heldThisTurn = false;
3655
- }
3857
+ clearMind2(mindBase);
3656
3858
  }
3657
3859
  function formatHoldNotice(channelLabel, unseen) {
3658
3860
  const lines = unseen.map((m) => {
@@ -3962,6 +4164,7 @@ var DeliveryManager = class {
3962
4164
  */
3963
4165
  clearMindSessions(mindName) {
3964
4166
  this.sessionStates.delete(mindName);
4167
+ clearMind2(mindName);
3965
4168
  const toDelete = [];
3966
4169
  for (const [bufferKey, buffer] of this.batchBuffers) {
3967
4170
  if (bufferKey.startsWith(`${mindName}:`)) {
@@ -4343,7 +4546,7 @@ var DeliveryManager = class {
4343
4546
  `To start hearing this channel, add a routing rule for "${channel}" to .config/routes.json.`,
4344
4547
  `Messages are held safely until you do \u2014 nothing is lost. If you'd rather not engage, just leave it unrouted.`
4345
4548
  ].filter((line) => line !== null).join("\n");
4346
- const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-E65T2HSW.js");
4549
+ const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-NDAW6YP7.js");
4347
4550
  await sendSystemMessage2(mindName, notification);
4348
4551
  }
4349
4552
  /**
@@ -4484,7 +4687,11 @@ var DeliveryManager = class {
4484
4687
  }
4485
4688
  }
4486
4689
  }
4487
- avatarBlocksCache.set(cacheKey, { blocks, expiresAt: Date.now() + AVATAR_CACHE_TTL });
4690
+ const now = Date.now();
4691
+ for (const [key, entry] of avatarBlocksCache) {
4692
+ if (entry.expiresAt <= now) avatarBlocksCache.delete(key);
4693
+ }
4694
+ avatarBlocksCache.set(cacheKey, { blocks, expiresAt: now + AVATAR_CACHE_TTL });
4488
4695
  return blocks;
4489
4696
  }
4490
4697
  incrementActive(mind, session, senders, channels) {
@@ -4517,6 +4724,9 @@ var DeliveryManager = class {
4517
4724
  const buffer = this.batchBuffers.get(bufferKey);
4518
4725
  if (buffer && buffer.messages.length > 0) {
4519
4726
  this.scheduleBatchTimers(mind, session, bufferKey);
4727
+ } else if (session.startsWith("new-")) {
4728
+ mindSessions.delete(session);
4729
+ if (mindSessions.size === 0) this.sessionStates.delete(mind);
4520
4730
  }
4521
4731
  }
4522
4732
  }
@@ -4657,40 +4867,78 @@ function resolveSleepAction(sleepBehavior, wokenByTrigger, wakeTriggerMatches) {
4657
4867
  if (!sleepBehavior && wakeTriggerMatches) return "queue-and-wake";
4658
4868
  return "queue";
4659
4869
  }
4660
- async function deliverMessage(mindName, payload) {
4870
+ async function deliverMessage(mindName, payload, opts = {}) {
4661
4871
  try {
4662
4872
  const baseName = await getBaseName(mindName);
4663
4873
  const entry = await findMind(baseName);
4664
4874
  if (!entry) {
4665
4875
  dlog2.warn(`cannot deliver to ${mindName}: mind not found`);
4666
- return;
4876
+ return false;
4667
4877
  }
4668
- const textContent = extractTextContent(payload.content);
4669
- await recordInbound(baseName, payload.channel, payload.sender ?? null, textContent);
4670
- const sleepManager = getSleepManagerIfReady();
4671
- if (sleepManager?.isSleeping(baseName)) {
4672
- const sleepState = sleepManager.getState(baseName);
4673
- const action = resolveSleepAction(
4674
- payload.whileSleeping,
4675
- sleepState.wokenByTrigger,
4676
- sleepManager.checkWakeTrigger(baseName, payload)
4677
- );
4678
- if (action === "skip") {
4679
- dlog2.info(
4680
- `skipped delivery to ${baseName} (sleeping, whileSleeping=skip, channel=${payload.channel})`
4878
+ if (!opts.isFlush) {
4879
+ const textContent = extractTextContent(payload.content);
4880
+ await recordInbound(baseName, payload.channel, payload.sender ?? null, textContent);
4881
+ const sleepManager = getSleepManagerIfReady();
4882
+ if (sleepManager?.isSleeping(baseName)) {
4883
+ const sleepState = sleepManager.getState(baseName);
4884
+ const action = resolveSleepAction(
4885
+ payload.whileSleeping,
4886
+ sleepState.wokenByTrigger,
4887
+ sleepManager.checkWakeTrigger(baseName, payload)
4681
4888
  );
4682
- return;
4683
- }
4684
- await sleepManager.queueSleepMessage(baseName, payload);
4685
- if (action === "queue-and-wake") {
4686
- sleepManager.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch((err) => dlog2.warn(`failed to trigger-wake ${baseName}`, logger_default.errorData(err)));
4889
+ if (action === "skip") {
4890
+ dlog2.info(
4891
+ `skipped delivery to ${baseName} (sleeping, whileSleeping=skip, channel=${payload.channel})`
4892
+ );
4893
+ return true;
4894
+ }
4895
+ await sleepManager.queueSleepMessage(baseName, payload);
4896
+ if (action === "queue-and-wake") {
4897
+ sleepManager.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch((err) => dlog2.warn(`failed to trigger-wake ${baseName}`, logger_default.errorData(err)));
4898
+ }
4899
+ return true;
4687
4900
  }
4688
- return;
4689
4901
  }
4690
4902
  const manager = getDeliveryManager();
4691
4903
  await manager.routeAndDeliver(mindName, payload);
4904
+ return true;
4692
4905
  } catch (err) {
4693
4906
  dlog2.warn(`unexpected error delivering to ${mindName}`, logger_default.errorData(err));
4907
+ return false;
4908
+ }
4909
+ }
4910
+ async function deliverBatch(mindName, payloads) {
4911
+ if (payloads.length === 0) return true;
4912
+ try {
4913
+ const baseName = await getBaseName(mindName);
4914
+ const entry = await findMind(baseName);
4915
+ if (!entry) {
4916
+ dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
4917
+ return false;
4918
+ }
4919
+ const channels = {};
4920
+ for (const p of payloads) {
4921
+ const ch = p.channel ?? "unknown";
4922
+ if (!channels[ch]) channels[ch] = [];
4923
+ channels[ch].push(p);
4924
+ }
4925
+ const first = payloads[0];
4926
+ const route = resolveRoute(getRoutingConfig(baseName), {
4927
+ channel: first.channel,
4928
+ sender: first.sender ?? void 0,
4929
+ isDM: first.isDM,
4930
+ participantCount: first.participantCount
4931
+ });
4932
+ const session = first.session ?? (route.destination === "mind" ? route.session : "main");
4933
+ const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
4934
+ method: "POST",
4935
+ headers: { "Content-Type": "application/json" },
4936
+ body: JSON.stringify({ session, batch: { channels } })
4937
+ });
4938
+ return res.ok;
4939
+ } catch (err) {
4940
+ dlog2.warn(`unexpected error delivering batch to ${mindName}`, logger_default.errorData(err));
4941
+ return false;
4694
4942
  }
4695
4943
  }
4696
4944
 
@@ -4700,6 +4948,12 @@ var dmCache = /* @__PURE__ */ new Map();
4700
4948
  function resetSystemDMCache() {
4701
4949
  dmCache.clear();
4702
4950
  }
4951
+ function shouldGenerateSystemFallback(opts) {
4952
+ const { senderIsMind, hasMessage, convType, participants, replyTarget } = opts;
4953
+ if (!senderIsMind || !hasMessage) return false;
4954
+ if (isSpiritName(replyTarget)) return false;
4955
+ return convType === "dm" && participants.length === 2 && participants.some((p) => p.userType === "system");
4956
+ }
4703
4957
  async function ensureSystemDM(mindName) {
4704
4958
  const cached = dmCache.get(mindName);
4705
4959
  if (cached) return { conversationId: cached };
@@ -4720,7 +4974,7 @@ async function ensureSystemDM(mindName) {
4720
4974
  return { conversationId: conv.id };
4721
4975
  }
4722
4976
  async function sendSystemMessage(mindName, text, opts) {
4723
- const isSpirit = mindName === "volute";
4977
+ const isSpirit = isSpiritName(mindName);
4724
4978
  let conversationId;
4725
4979
  if (!isSpirit) {
4726
4980
  const dm = await ensureSystemDM(mindName);
@@ -4740,32 +4994,29 @@ async function sendSystemMessage(mindName, text, opts) {
4740
4994
  });
4741
4995
  }
4742
4996
  async function sendSystemMessageDirect(mindName, text) {
4997
+ if (isSpiritName(mindName)) {
4998
+ await recordInbound(mindName, "@volute", "volute", text);
4999
+ return {};
5000
+ }
4743
5001
  const { conversationId } = await ensureSystemDM(mindName);
4744
5002
  await addMessage(conversationId, "user", "volute", [{ type: "text", text }]);
4745
5003
  await recordInbound(mindName, "@volute", "volute", text);
4746
5004
  return { conversationId };
4747
5005
  }
4748
- async function isSpiritAvailable() {
4749
- const spiritEntry = await findMind("volute");
4750
- return !!(spiritEntry?.running && spiritEntry.mindType === "spirit");
4751
- }
4752
- async function generateSystemReply(conversationId, mindName, message) {
4753
- if (await isSpiritAvailable()) {
4754
- try {
4755
- await deliverMessage("volute", {
4756
- content: [{ type: "text", text: message }],
4757
- channel: `@${mindName}`,
4758
- conversationId,
4759
- sender: mindName,
4760
- isDM: true,
4761
- participants: ["volute", mindName],
4762
- participantCount: 2
4763
- });
4764
- return;
4765
- } catch (err) {
4766
- slog4.warn(`failed to route to spirit, falling back to aiCompleteUtility`, logger_default.errorData(err));
4767
- }
5006
+ async function spiritWillHandle() {
5007
+ const spiritEntry = await findMind(SPIRIT_NAME);
5008
+ if (spiritEntry?.running && spiritEntry.mindType === "spirit") return true;
5009
+ try {
5010
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
5011
+ const sm = getSleepManagerIfReady2();
5012
+ if (sm?.isSleeping(SPIRIT_NAME)) return true;
5013
+ } catch (err) {
5014
+ slog4.debug("could not check spirit sleep state", logger_default.errorData(err));
4768
5015
  }
5016
+ return false;
5017
+ }
5018
+ async function generateSystemFallbackReply(conversationId, mindName, message) {
5019
+ if (await spiritWillHandle()) return;
4769
5020
  const entry = await findMind(mindName);
4770
5021
  const dir = mindDir(mindName);
4771
5022
  const config = readVoluteConfig(dir);
@@ -4784,7 +5035,7 @@ async function generateSystemReply(conversationId, mindName, message) {
4784
5035
  if (config.sleep.schedule?.wake) contextParts.push(`Wake cron: ${config.sleep.schedule.wake}`);
4785
5036
  }
4786
5037
  try {
4787
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-YAHDOIU5.js");
5038
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
4788
5039
  const sm = getSleepManagerIfReady2();
4789
5040
  if (sm) {
4790
5041
  const state2 = sm.getState(mindName);
@@ -4830,10 +5081,11 @@ async function generateSystemReply(conversationId, mindName, message) {
4830
5081
 
4831
5082
  export {
4832
5083
  resetSystemDMCache,
5084
+ shouldGenerateSystemFallback,
4833
5085
  ensureSystemDM,
4834
5086
  sendSystemMessage,
4835
5087
  sendSystemMessageDirect,
4836
- generateSystemReply,
5088
+ generateSystemFallbackReply,
4837
5089
  checkHealth,
4838
5090
  RotatingLog,
4839
5091
  syncProviderToMinds,
@@ -4892,6 +5144,7 @@ export {
4892
5144
  wakeMind,
4893
5145
  startSpiritFull,
4894
5146
  stopSpiritFull,
5147
+ buildSeedOrientation,
4895
5148
  stopMindFull,
4896
5149
  matchesGlob,
4897
5150
  SleepManager,
@@ -4903,6 +5156,7 @@ export {
4903
5156
  linkToolResultToTurn,
4904
5157
  resolveSleepAction,
4905
5158
  deliverMessage,
5159
+ deliverBatch,
4906
5160
  ensureSystemChannel,
4907
5161
  joinSystemChannel,
4908
5162
  announceToSystem