volute 0.45.2 → 0.47.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 (146) hide show
  1. package/README.md +4 -4
  2. package/dist/{activity-events-BUR2RYTJ.js → activity-events-MIOK3IOU.js} +3 -3
  3. package/dist/{ai-service-UODCCE4O.js → ai-service-CVZEYKTK.js} +4 -4
  4. package/dist/{archive-WS2L2VIQ.js → archive-6Z4GFQPH.js} +3 -3
  5. package/dist/{auth-FLAEXXZK.js → auth-A4VGHADI.js} +4 -4
  6. package/dist/avatar-image-7S3WA77A.js +22 -0
  7. package/dist/{backup-TST5C2YN.js → backup-IIAFOW2L.js} +13 -13
  8. package/dist/channels-ITE4YC57.js +89 -0
  9. package/dist/{chat-76MRWWQX.js → chat-AJLJCQ3M.js} +12 -8
  10. package/dist/{chunk-ZV7MFISX.js → chunk-2MJ7EOMT.js} +20 -2
  11. package/dist/{chunk-R77O2WMK.js → chunk-2OIYNHWO.js} +1 -1
  12. package/dist/{chunk-QCN24XEN.js → chunk-3YUBFTDN.js} +1 -1
  13. package/dist/{chunk-NYC5U6K7.js → chunk-4I72V3E5.js} +10 -10
  14. package/dist/{chunk-QNQMSWZE.js → chunk-5IERLDUS.js} +3 -3
  15. package/dist/{chunk-FXORA5JG.js → chunk-5NN2VHJV.js} +3 -3
  16. package/dist/chunk-7KRIHYLR.js +124 -0
  17. package/dist/{chunk-RRNXJOKB.js → chunk-BGYHLR54.js} +2 -2
  18. package/dist/{chunk-QMJCNZ6C.js → chunk-C3KLHJDB.js} +1 -1
  19. package/dist/{chunk-TQJ6ZYVY.js → chunk-CBOIHNMR.js} +2 -2
  20. package/dist/{chunk-H2X54EHC.js → chunk-DGGZVZTM.js} +1 -1
  21. package/dist/{chunk-4JOQZPCE.js → chunk-EIQDZC43.js} +1 -1
  22. package/dist/{chunk-YT7PEESJ.js → chunk-F55GRIEK.js} +464 -221
  23. package/dist/{chunk-OP4JG2BS.js → chunk-HPGN6XRT.js} +8 -4
  24. package/dist/{chunk-7ZUB63PS.js → chunk-IWJSNMSY.js} +5 -5
  25. package/dist/{chunk-ZDZISKCL.js → chunk-IYG4FBMP.js} +1 -1
  26. package/dist/chunk-JCMGDCGX.js +237 -0
  27. package/dist/{clock-K43IMROX.js → chunk-JSVHH32K.js} +26 -21
  28. package/dist/{chunk-UYLGEDVD.js → chunk-K23P5Q5N.js} +340 -339
  29. package/dist/{chunk-7C44R625.js → chunk-LTLFOSA4.js} +49 -3
  30. package/dist/{chunk-IHKGWTGT.js → chunk-LWDTP6DH.js} +1 -1
  31. package/dist/{chunk-JZ62SYWR.js → chunk-M3XDVS5Q.js} +1 -1
  32. package/dist/{chunk-FR6S2KNN.js → chunk-M6TTZRGH.js} +9 -9
  33. package/dist/{chunk-EJVPTVTI.js → chunk-MTRMQFCQ.js} +1 -1
  34. package/dist/{chunk-T6HYVHPG.js → chunk-ODDGGLM6.js} +3 -3
  35. package/dist/{chunk-QLWG3C2Z.js → chunk-RRZ4RQQU.js} +2 -2
  36. package/dist/{chunk-CDPBTTZO.js → chunk-RUFLGTOT.js} +2 -2
  37. package/dist/{chunk-F7H6ATM4.js → chunk-SKNXMUIU.js} +1 -1
  38. package/dist/{chunk-RXEEYDIB.js → chunk-SZJCJVXV.js} +1 -1
  39. package/dist/{chunk-DRJ24GXK.js → chunk-VMCZBXF2.js} +52 -8
  40. package/dist/{chunk-ALKYQULU.js → chunk-WW6C3PLV.js} +1 -1
  41. package/dist/{chunk-A7WQZKOU.js → chunk-XHN6XBWV.js} +2 -2
  42. package/dist/{chunk-6EMWJZBK.js → chunk-YKA7DCVW.js} +1 -1
  43. package/dist/cli.js +24 -24
  44. package/dist/clock-E6N5Y3YW.js +16 -0
  45. package/dist/{cloud-sync-3OJOPZRT.js → cloud-sync-LQTVDTQ3.js} +19 -17
  46. package/dist/{conversations-JZTD6KLR.js → conversations-C7IKYSHD.js} +4 -4
  47. package/dist/{create-IOV4GCRX.js → create-PQQXKTJK.js} +1 -1
  48. package/dist/{daemon-restart-TIHKVVVA.js → daemon-restart-5F3YTLZK.js} +7 -7
  49. package/dist/daemon.js +418 -273
  50. package/dist/{db-MF2JNRUW.js → db-FO6LXYCW.js} +2 -2
  51. package/dist/{delete-BFMIGAIB.js → delete-HPLC7ETU.js} +1 -1
  52. package/dist/delivery-manager-TN5KK2AP.js +34 -0
  53. package/dist/{delivery-router-N4A7ZC5T.js → delivery-router-3QFG2ZEX.js} +3 -3
  54. package/dist/{down-U2CZ23M4.js → down-ZI4S6265.js} +5 -5
  55. package/dist/echo-text-SEQCLGAT.js +34 -0
  56. package/dist/{exec-G3ZRST4Z.js → exec-QIFYZG4C.js} +3 -3
  57. package/dist/{export-GGIVO76O.js → export-4GVGNW54.js} +6 -6
  58. package/dist/{extensions-JONTW2HF.js → extensions-PIMIQ7J3.js} +21 -17
  59. package/dist/{files-4LN3EN7Q.js → files-FEFZKMNM.js} +6 -6
  60. package/dist/{import-42YNQBF4.js → import-7MO3YTRE.js} +6 -6
  61. package/dist/{isolation-YMGZZ4E5.js → isolation-MCT4ZYLT.js} +3 -3
  62. package/dist/{login-M6OJUPQ2.js → login-OF4WXDAN.js} +5 -5
  63. package/dist/{logout-WQBWO77G.js → logout-ERE2BGA3.js} +5 -5
  64. package/dist/message-delivery-ZE3YGJDD.js +38 -0
  65. package/dist/{mind-DWB6XYCH.js → mind-POOHFZHP.js} +18 -18
  66. package/dist/{mind-activity-tracker-POBLU3LC.js → mind-activity-tracker-YVZPC4A5.js} +4 -4
  67. package/dist/{mind-list-4D7J4CHD.js → mind-list-KEGRGQEQ.js} +5 -4
  68. package/dist/mind-service-JFY7UG5E.js +40 -0
  69. package/dist/mind-sleep-IN3WPA76.js +83 -0
  70. package/dist/{mind-status-UO34ASXZ.js → mind-status-SMUWJKGV.js} +20 -3
  71. package/dist/{package-A7KXT67Q.js → package-WPEE2HVJ.js} +3 -2
  72. package/dist/{registry-OUZBZ3Q4.js → registry-UMMDRPRM.js} +2 -2
  73. package/dist/{sandbox-ZJ7QA6Z6.js → sandbox-G4X4K3UN.js} +4 -4
  74. package/dist/scheduler-HQSDYJFY.js +32 -0
  75. package/dist/{schema-PVNG3PQM.js → schema-VEVO7HG6.js} +3 -1
  76. package/dist/{seed-HCP5NYKH.js → seed-VANPU43L.js} +1 -1
  77. package/dist/{seed-cmd-ILOGNIFB.js → seed-cmd-SYVDZKM4.js} +2 -2
  78. package/dist/{seed-create-OURS2LS4.js → seed-create-3FGZPS6C.js} +5 -5
  79. package/dist/{seed-sprout-ELDFDAN6.js → seed-sprout-H7QLFPFM.js} +13 -11
  80. package/dist/{send-AAIOPJAV.js → send-HYDFGC6G.js} +13 -7
  81. package/dist/{service-UOCLXDWR.js → service-SYJ5G6LP.js} +4 -4
  82. package/dist/{service-install-4HHFDDWY.js → service-install-FQ43Z4DF.js} +5 -5
  83. package/dist/{setup-Y2EJI3GG.js → setup-KFYVWTXG.js} +3 -3
  84. package/dist/{setup-DQM5VPY4.js → setup-QJHGMGSN.js} +7 -7
  85. package/dist/skills/orientation/SKILL.md +1 -1
  86. package/dist/skills/volute-mind/SKILL.md +8 -8
  87. package/dist/skills/volute-mind/references/sleep.md +8 -1
  88. package/dist/{skills-BKINFL7W.js → skills-L2CMTW7A.js} +5 -5
  89. package/dist/sleep-manager-3NA7ZJP3.js +36 -0
  90. package/dist/{spirit-DBTIW7P4.js → spirit-WXN4IDBF.js} +8 -8
  91. package/dist/{sprout-W3KC7UGK.js → sprout-LTL5P36T.js} +1 -1
  92. package/dist/{status-SWRVGXBI.js → status-V4RJMHJ4.js} +19 -9
  93. package/dist/system-chat-N2F7HVY3.js +38 -0
  94. package/dist/{systems-VNB5WKSS.js → systems-B5YJKMW4.js} +6 -6
  95. package/dist/{tailscale-3XOKOW7T.js → tailscale-MCLY5GYE.js} +2 -2
  96. package/dist/{template-hash-446HHH5V.js → template-hash-GSOUARW4.js} +2 -2
  97. package/dist/template-staleness-EMQORSGG.js +15 -0
  98. package/dist/{turn-tracker-2W6Q2NSR.js → turn-tracker-JZRZDFWV.js} +6 -5
  99. package/dist/{up-FTK7CTO3.js → up-J2PBPOZL.js} +6 -6
  100. package/dist/{update-SWAYZRWJ.js → update-ACV4SAY3.js} +5 -5
  101. package/dist/{update-check-PQ2MZ47K.js → update-check-266R4ZSY.js} +3 -3
  102. package/dist/{version-notify-3XMAIQTE.js → version-notify-XRGTEEDK.js} +42 -23
  103. package/dist/web-assets/assets/index-DtctoDXY.css +1 -0
  104. package/dist/web-assets/assets/index-Wpyb2CML.js +86 -0
  105. package/dist/web-assets/index.html +2 -2
  106. package/drizzle/0011_drop_turn_summary_event_id.sql +3 -0
  107. package/drizzle/0012_channel_gates.sql +17 -0
  108. package/drizzle/meta/_journal.json +14 -0
  109. package/package.json +3 -2
  110. package/templates/_base/home/VOLUTE.md +6 -2
  111. package/templates/_base/src/lib/context-breakdown.ts +57 -67
  112. package/templates/_base/src/lib/startup.ts +43 -2
  113. package/templates/_base/src/lib/volute-server.ts +2 -2
  114. package/templates/claude/src/agent.ts +105 -43
  115. package/templates/claude/src/lib/session-reaper.ts +63 -0
  116. package/templates/claude/src/server.ts +10 -1
  117. package/templates/codex/src/agent.ts +62 -40
  118. package/templates/pi/src/agent.ts +55 -38
  119. package/dist/avatar-image-6KY6UU4D.js +0 -16
  120. package/dist/delivery-manager-VDVKVA46.js +0 -32
  121. package/dist/echo-text-6JJOPUD4.js +0 -32
  122. package/dist/message-delivery-3C65ZI6L.js +0 -34
  123. package/dist/mind-service-3F4I46CI.js +0 -36
  124. package/dist/mind-sleep-YMBR3I35.js +0 -47
  125. package/dist/scheduler-P6DXLLXI.js +0 -30
  126. package/dist/sleep-manager-HCUH4PM4.js +0 -34
  127. package/dist/system-chat-XOALE2RD.js +0 -36
  128. package/dist/web-assets/assets/index-B1JBQv2m.css +0 -1
  129. package/dist/web-assets/assets/index-Bc2m8gRC.js +0 -86
  130. package/dist/{accept-N5XULCXK.js → accept-YAFP5JFJ.js} +3 -3
  131. package/dist/{bridge-2PO3AI5D.js → bridge-TIZGSXOD.js} +3 -3
  132. package/dist/{create-DF3KISTW.js → create-A7PDHIIL.js} +3 -3
  133. package/dist/{env-VZ4MLFQW.js → env-HRL2PWAT.js} +5 -5
  134. package/dist/{extension-SEID47SR.js → extension-QBY4C44L.js} +3 -3
  135. package/dist/{list-OXIAY3W7.js → list-LQO2DCIK.js} +3 -3
  136. package/dist/{login-MY6ETFYB.js → login-6TLMI2TQ.js} +3 -3
  137. package/dist/{logout-NCG3TMDL.js → logout-BK53HXNY.js} +3 -3
  138. package/dist/{mind-history-VRO5A76U.js → mind-history-47DN6ILW.js} +4 -4
  139. package/dist/{mind-wake-LCLGTXA2.js → mind-wake-R54BPESY.js} +3 -3
  140. package/dist/{read-QVAXMTPV.js → read-PWAJOQIQ.js} +3 -3
  141. package/dist/{register-Y2H4PY5R.js → register-7SZJFHU6.js} +3 -3
  142. package/dist/{reject-EV35X64Y.js → reject-6VH7N2A4.js} +3 -3
  143. package/dist/{restart-FJQ662XJ.js → restart-H5WJDMA7.js} +4 -4
  144. package/dist/{skill-FKFHIJRR.js → skill-GUKG2YVH.js} +5 -5
  145. package/dist/{start-5JE3TV66.js → start-X46I3GXB.js} +4 -4
  146. package/dist/{stop-AAMTG3TU.js → stop-HJQV4YSM.js} +4 -4
@@ -1,16 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadMergedEnv
4
- } from "./chunk-ZDZISKCL.js";
4
+ } from "./chunk-IYG4FBMP.js";
5
5
  import {
6
6
  spiritDir
7
- } from "./chunk-FR6S2KNN.js";
7
+ } from "./chunk-M6TTZRGH.js";
8
+ import {
9
+ clearMind,
10
+ linkInboundToActiveTurn,
11
+ publish as publish3,
12
+ summarizeOrphanedTurns
13
+ } from "./chunk-K23P5Q5N.js";
14
+ import {
15
+ getPrompt
16
+ } from "./chunk-JCMGDCGX.js";
8
17
  import {
9
18
  hashSkillDir,
10
19
  importSkillFromDir,
11
20
  removeSharedSkill,
12
21
  sharedSkillsDir
13
- } from "./chunk-7ZUB63PS.js";
22
+ } from "./chunk-IWJSNMSY.js";
14
23
  import {
15
24
  addMessage,
16
25
  createChannel,
@@ -20,61 +29,57 @@ import {
20
29
  getParticipants,
21
30
  joinChannel,
22
31
  publish as publish2
23
- } from "./chunk-OP4JG2BS.js";
24
- import {
25
- readVoluteConfig,
26
- resolveWakeTriggers,
27
- writeVoluteConfig
28
- } from "./chunk-PFZBNKUH.js";
32
+ } from "./chunk-HPGN6XRT.js";
29
33
  import {
30
34
  isSandboxEnabled,
31
35
  wrapForSandbox
32
- } from "./chunk-JZ62SYWR.js";
36
+ } from "./chunk-M3XDVS5Q.js";
33
37
  import {
34
38
  markIdle,
35
39
  onMindEvent
36
- } from "./chunk-EJVPTVTI.js";
40
+ } from "./chunk-MTRMQFCQ.js";
41
+ import {
42
+ renderAvatarBlock
43
+ } from "./chunk-VMCZBXF2.js";
44
+ import {
45
+ readVoluteConfig,
46
+ resolveWakeTriggers,
47
+ writeVoluteConfig
48
+ } from "./chunk-PFZBNKUH.js";
37
49
  import {
38
50
  extractTextContent,
39
51
  getRoutingConfig,
40
52
  resolveDeliveryMode,
41
53
  resolveRoute,
42
54
  setRoutesChangeListener
43
- } from "./chunk-4JOQZPCE.js";
44
- import {
45
- clearMind,
46
- getPrompt,
47
- linkInboundToActiveTurn,
48
- publish as publish3,
49
- summarizeOrphanedTurns
50
- } from "./chunk-UYLGEDVD.js";
55
+ } from "./chunk-EIQDZC43.js";
51
56
  import {
52
57
  chownMindDir,
53
58
  exec,
54
59
  isIsolationEnabled,
55
60
  mindUserName,
56
61
  wrapForIsolation
57
- } from "./chunk-H2X54EHC.js";
62
+ } from "./chunk-DGGZVZTM.js";
58
63
  import {
59
64
  aiCompleteUtility,
60
65
  getAiConfig,
61
66
  resolveApiKey
62
- } from "./chunk-IHKGWTGT.js";
67
+ } from "./chunk-LWDTP6DH.js";
63
68
  import {
64
69
  readGlobalConfig,
65
70
  writeGlobalConfig
66
- } from "./chunk-F7H6ATM4.js";
71
+ } from "./chunk-SKNXMUIU.js";
67
72
  import {
68
73
  getOrCreateMindUser,
69
74
  getOrCreateSystemUser,
70
75
  getUser,
71
76
  getUserByUsername,
72
77
  syncMindProfile
73
- } from "./chunk-T6HYVHPG.js";
78
+ } from "./chunk-ODDGGLM6.js";
74
79
  import {
75
80
  publish,
76
81
  subscribe
77
- } from "./chunk-7C44R625.js";
82
+ } from "./chunk-LTLFOSA4.js";
78
83
  import {
79
84
  logger_default
80
85
  } from "./chunk-T2TP6ZC6.js";
@@ -90,18 +95,19 @@ import {
90
95
  stateDir,
91
96
  voluteHome,
92
97
  voluteSystemDir
93
- } from "./chunk-ALKYQULU.js";
98
+ } from "./chunk-WW6C3PLV.js";
94
99
  import {
95
100
  activity,
101
+ channelGates,
96
102
  deliveryQueue,
97
103
  messages,
98
104
  mindHistory,
99
105
  mindNotices,
100
106
  sessions
101
- } from "./chunk-ZV7MFISX.js";
107
+ } from "./chunk-2MJ7EOMT.js";
102
108
 
103
109
  // packages/daemon/src/lib/delivery/message-delivery.ts
104
- import { and as and5, eq as eq6, inArray as inArray2 } from "drizzle-orm";
110
+ import { and as and5, eq as eq6, inArray as inArray3 } from "drizzle-orm";
105
111
 
106
112
  // packages/daemon/src/lib/daemon/sleep-manager.ts
107
113
  import { execFile as execFile2, spawn as spawnChild } from "child_process";
@@ -117,7 +123,7 @@ import {
117
123
  import { resolve as resolve7 } from "path";
118
124
  import { promisify as promisify2 } from "util";
119
125
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
120
- import { and as and2, eq as eq3 } from "drizzle-orm";
126
+ import { and as and2, eq as eq3, inArray } from "drizzle-orm";
121
127
 
122
128
  // packages/daemon/src/lib/daemon/mind-manager.ts
123
129
  import { execFile, spawn } from "child_process";
@@ -769,21 +775,8 @@ var MindManager = class {
769
775
  if (isIsolationEnabled()) {
770
776
  env.HOME = resolve2(dir, "home");
771
777
  }
772
- const customNode = process.env.VOLUTE_NODE_PATH;
773
- let baseBin;
774
- let baseArgs;
775
- if (customNode) {
776
- baseBin = customNode;
777
- baseArgs = [
778
- resolve2(dir, "node_modules", ".bin", "tsx"),
779
- "src/server.ts",
780
- "--port",
781
- String(port)
782
- ];
783
- } else {
784
- baseBin = resolve2(dir, "node_modules", ".bin", "tsx");
785
- baseArgs = ["src/server.ts", "--port", String(port)];
786
- }
778
+ const baseBin = process.env.VOLUTE_NODE_PATH ?? process.execPath;
779
+ const baseArgs = ["--import", "tsx", "src/server.ts", "--port", String(port)];
787
780
  let spawnCmd;
788
781
  let spawnArgs;
789
782
  if (isIsolationEnabled()) {
@@ -945,7 +938,7 @@ var MindManager = class {
945
938
  if (this.shuttingDown || this.stopping.has(name)) return;
946
939
  mlog.error(`mind ${name} exited with code ${code}`);
947
940
  try {
948
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-HCUH4PM4.js");
941
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-3NA7ZJP3.js");
949
942
  const sleepMgr = getSleepManagerIfReady2();
950
943
  const sleepState = sleepMgr?.getState(name);
951
944
  if (sleepState?.sleeping) {
@@ -975,15 +968,15 @@ var MindManager = class {
975
968
  (err) => mlog.warn(`failed to clear turn state for ${name} after crash`, logger_default.errorData(err))
976
969
  );
977
970
  try {
978
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-VDVKVA46.js");
971
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-TN5KK2AP.js");
979
972
  getDeliveryManager2().clearMindSessions(name);
980
973
  } catch (err) {
981
974
  if (!(err instanceof Error && err.message.includes("not initialized"))) {
982
975
  mlog.warn(`failed to clear delivery state for ${name} after crash`, logger_default.errorData(err));
983
976
  }
984
977
  }
985
- import("./mind-activity-tracker-POBLU3LC.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
986
- import("./activity-events-BUR2RYTJ.js").then(
978
+ import("./mind-activity-tracker-YVZPC4A5.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
979
+ import("./activity-events-MIOK3IOU.js").then(
987
980
  ({ publish: publish4 }) => publish4({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
988
981
  ).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
989
982
  const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
@@ -1041,7 +1034,7 @@ var MindManager = class {
1041
1034
  mlog.warn(`failed to clear turn state for ${name} on stop`, logger_default.errorData(err));
1042
1035
  }
1043
1036
  try {
1044
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-VDVKVA46.js");
1037
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-TN5KK2AP.js");
1045
1038
  getDeliveryManager2().clearMindSessions(name);
1046
1039
  } catch (err) {
1047
1040
  if (!(err instanceof Error && err.message.includes("not initialized"))) {
@@ -1049,7 +1042,7 @@ var MindManager = class {
1049
1042
  }
1050
1043
  }
1051
1044
  try {
1052
- const { clearEchoTextCache } = await import("./echo-text-6JJOPUD4.js");
1045
+ const { clearEchoTextCache } = await import("./echo-text-SEQCLGAT.js");
1053
1046
  clearEchoTextCache(name);
1054
1047
  } catch (err) {
1055
1048
  mlog.debug(`failed to clear echo-text cache for ${name}`, logger_default.errorData(err));
@@ -1226,6 +1219,10 @@ async function getSessionUserId(sessionId) {
1226
1219
  return row.userId;
1227
1220
  }
1228
1221
  async function cleanExpiredSessions() {
1222
+ const now = Date.now();
1223
+ for (const [id, cached] of sessionCache) {
1224
+ if (cached.expires <= now) sessionCache.delete(id);
1225
+ }
1229
1226
  const db = await getDb();
1230
1227
  const cutoff = Date.now() - SESSION_MAX_AGE;
1231
1228
  await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
@@ -1369,10 +1366,54 @@ function deleteSystemsConfig() {
1369
1366
  }
1370
1367
  }
1371
1368
 
1369
+ // packages/daemon/src/lib/util/sanitize-svg.ts
1370
+ import DOMPurify from "isomorphic-dompurify";
1371
+ var ALLOWED_TAGS = ["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g"];
1372
+ var ALLOWED_ATTR = [
1373
+ "viewBox",
1374
+ "fill",
1375
+ "fill-opacity",
1376
+ "fill-rule",
1377
+ "stroke",
1378
+ "stroke-width",
1379
+ "stroke-linecap",
1380
+ "stroke-linejoin",
1381
+ "stroke-dasharray",
1382
+ "stroke-opacity",
1383
+ "opacity",
1384
+ "d",
1385
+ "cx",
1386
+ "cy",
1387
+ "r",
1388
+ "x",
1389
+ "y",
1390
+ "width",
1391
+ "height",
1392
+ "points",
1393
+ "transform"
1394
+ ];
1395
+ function sanitizeSvgIcon(html) {
1396
+ return DOMPurify.sanitize(html, {
1397
+ ALLOWED_TAGS,
1398
+ ALLOWED_ATTR,
1399
+ FORBID_TAGS: ["script", "foreignObject"],
1400
+ FORBID_ATTR: ["href", "xlink:href"]
1401
+ });
1402
+ }
1403
+
1372
1404
  // packages/daemon/src/lib/extensions.ts
1373
1405
  var VALID_EXTENSION_ID = /^[a-z0-9][a-z0-9_-]*$/;
1374
1406
  var loaded = [];
1375
1407
  var discovered = [];
1408
+ function enrichActivityMetadata(manifest, metadata) {
1409
+ const enriched = {
1410
+ ...metadata,
1411
+ ...manifest.icon && !metadata?.icon ? { icon: manifest.icon } : {},
1412
+ ...manifest.color && !metadata?.color ? { color: manifest.color } : {}
1413
+ };
1414
+ if (typeof enriched.icon === "string") enriched.icon = sanitizeSvgIcon(enriched.icon);
1415
+ return enriched;
1416
+ }
1376
1417
  function toCommandInfo(cmd) {
1377
1418
  const { handler: _, ...info } = cmd;
1378
1419
  return info;
@@ -1476,14 +1517,8 @@ async function buildContext(manifest, dataDir, authMw) {
1476
1517
  getUser: async (id) => getUser(id),
1477
1518
  getUserByUsername: async (username) => getUserByUsername(username),
1478
1519
  publishActivity: (event) => {
1479
- const enriched = {
1480
- ...event,
1481
- metadata: {
1482
- ...event.metadata,
1483
- ...manifest.icon && !event.metadata?.icon ? { icon: manifest.icon } : {},
1484
- ...manifest.color && !event.metadata?.color ? { color: manifest.color } : {}
1485
- }
1486
- };
1520
+ const metadata = enrichActivityMetadata(manifest, event.metadata);
1521
+ const enriched = { ...event, metadata };
1487
1522
  publish(enriched).catch(
1488
1523
  (err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
1489
1524
  );
@@ -1561,14 +1596,8 @@ async function loadExtension(manifest, app, authMw) {
1561
1596
  const result = await cmd.handler(parsed, {
1562
1597
  ...context,
1563
1598
  publishActivity: (rawEvent) => {
1564
- const event = {
1565
- ...rawEvent,
1566
- metadata: {
1567
- ...rawEvent.metadata,
1568
- ...manifest.icon && !rawEvent.metadata?.icon ? { icon: manifest.icon } : {},
1569
- ...manifest.color && !rawEvent.metadata?.color ? { color: manifest.color } : {}
1570
- }
1571
- };
1599
+ const metadata = enrichActivityMetadata(manifest, rawEvent.metadata);
1600
+ const event = { ...rawEvent, metadata };
1572
1601
  activityPromises.push(
1573
1602
  publish(event).catch((err) => {
1574
1603
  logger_default.error(
@@ -1608,7 +1637,7 @@ async function loadExtension(manifest, app, authMw) {
1608
1637
  }
1609
1638
  if (resolvedAssetsDir && existsSync6(resolvedAssetsDir)) {
1610
1639
  const assetsDir = resolvedAssetsDir;
1611
- const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
1640
+ const { readFile, stat: fsStat } = await import("fs/promises");
1612
1641
  const { extname: ext } = await import("path");
1613
1642
  const mimeTypes = {
1614
1643
  ".html": "text/html",
@@ -1633,11 +1662,11 @@ async function loadExtension(manifest, app, authMw) {
1633
1662
  const s = await fsStat(filePath).catch(() => null);
1634
1663
  if (s?.isFile()) {
1635
1664
  const mime = mimeTypes[ext(filePath)] || "application/octet-stream";
1636
- const body = await readFile2(filePath);
1665
+ const body = await readFile(filePath);
1637
1666
  return c.body(body, 200, { "Content-Type": mime });
1638
1667
  }
1639
1668
  if (existsSync6(indexPath)) {
1640
- const body = await readFile2(indexPath, "utf-8");
1669
+ const body = await readFile(indexPath, "utf-8");
1641
1670
  return c.html(body);
1642
1671
  }
1643
1672
  return c.text("Not found", 404);
@@ -1955,7 +1984,7 @@ async function installNpmExtension(pkg) {
1955
1984
  throw new Error(`Extension "${pkg}" is already installed`);
1956
1985
  }
1957
1986
  const dir = ensureExtensionsNpmDir();
1958
- const { exec: exec2 } = await import("./exec-G3ZRST4Z.js");
1987
+ const { exec: exec2 } = await import("./exec-QIFYZG4C.js");
1959
1988
  try {
1960
1989
  await exec2("npm", ["install", "--ignore-scripts", pkg], { cwd: dir });
1961
1990
  } catch (err) {
@@ -1976,7 +2005,7 @@ async function uninstallNpmExtension(pkg) {
1976
2005
  packages.splice(idx, 1);
1977
2006
  writeExtensionsConfig(packages);
1978
2007
  try {
1979
- const { exec: exec2 } = await import("./exec-G3ZRST4Z.js");
2008
+ const { exec: exec2 } = await import("./exec-QIFYZG4C.js");
1980
2009
  await exec2("npm", ["uninstall", "--ignore-scripts", pkg], { cwd: extensionsNpmDir() });
1981
2010
  } catch (err) {
1982
2011
  logger_default.warn(
@@ -2306,6 +2335,7 @@ async function ensureMailAddress(mindName) {
2306
2335
  import { resolve as resolve5 } from "path";
2307
2336
  import { CronExpressionParser } from "cron-parser";
2308
2337
  var slog2 = logger_default.child("scheduler");
2338
+ var CATCHUP_STALE_MINUTES = 10;
2309
2339
  var Scheduler = class {
2310
2340
  schedules = /* @__PURE__ */ new Map();
2311
2341
  mindDirs = /* @__PURE__ */ new Map();
@@ -2345,6 +2375,26 @@ var Scheduler = class {
2345
2375
  } else {
2346
2376
  this.schedules.delete(mindName);
2347
2377
  }
2378
+ const epochMinute = Math.floor(Date.now() / 6e4);
2379
+ const validKeys = new Set(schedules.map((s) => `${mindName}:${s.id}`));
2380
+ let changed = false;
2381
+ for (const key of this.lastFired.keys()) {
2382
+ if (key.startsWith(`${mindName}:`) && !validKeys.has(key)) {
2383
+ this.lastFired.delete(key);
2384
+ changed = true;
2385
+ }
2386
+ }
2387
+ for (const key of validKeys) {
2388
+ if (!this.lastFired.has(key)) {
2389
+ this.lastFired.set(key, epochMinute);
2390
+ changed = true;
2391
+ }
2392
+ }
2393
+ if (changed) {
2394
+ this.saveState().catch(
2395
+ (err) => slog2.warn(`failed to persist scheduler state for ${mindName}`, logger_default.errorData(err))
2396
+ );
2397
+ }
2348
2398
  }
2349
2399
  unloadSchedules(mindName) {
2350
2400
  this.schedules.delete(mindName);
@@ -2390,11 +2440,14 @@ var Scheduler = class {
2390
2440
  return false;
2391
2441
  }
2392
2442
  }
2393
- if (prevMinute === epochMinute) {
2394
- this.lastFired.set(key, epochMinute);
2395
- return true;
2443
+ const lastFired = this.lastFired.get(key) ?? epochMinute;
2444
+ if (prevMinute <= lastFired) return false;
2445
+ this.lastFired.set(key, prevMinute);
2446
+ if (epochMinute - prevMinute > CATCHUP_STALE_MINUTES) {
2447
+ slog2.info(`skipping stale catch-up fire for ${key} (${epochMinute - prevMinute}min late)`);
2448
+ return false;
2396
2449
  }
2397
- return false;
2450
+ return true;
2398
2451
  }
2399
2452
  async fire(mindName, schedule) {
2400
2453
  try {
@@ -2739,11 +2792,7 @@ async function startMindFull(name) {
2739
2792
  if (entry?.parent) return;
2740
2793
  if (!entry || entry.stage === "seed") {
2741
2794
  if (entry?.stage === "seed") {
2742
- const creatorNote = entry.createdBy ? ` Your creator is ${entry.createdBy}. Send them a message to introduce yourself.` : "";
2743
- sendSystemMessage(
2744
- baseName,
2745
- `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}`
2746
- ).catch(
2795
+ sendSeedOrientation(baseName, entry.createdBy).catch(
2747
2796
  (err) => logger_default.error(`failed to send seed orientation for ${baseName}`, logger_default.errorData(err))
2748
2797
  );
2749
2798
  if (entry.createdBy) {
@@ -2805,7 +2854,7 @@ async function wakeMind(name) {
2805
2854
  async function startSpiritFull(name) {
2806
2855
  const entry = await findMind(name);
2807
2856
  if (entry?.dir) {
2808
- const { registerMindDir } = await import("./delivery-router-N4A7ZC5T.js");
2857
+ const { registerMindDir } = await import("./delivery-router-3QFG2ZEX.js");
2809
2858
  registerMindDir(name, entry.dir);
2810
2859
  }
2811
2860
  await getMindManager().startMind(name);
@@ -2826,9 +2875,23 @@ async function stopSpiritFull(name) {
2826
2875
  summary: `${name} spirit stopped`
2827
2876
  }).catch((err) => logger_default.error("failed to publish spirit_stopped activity", logger_default.errorData(err)));
2828
2877
  }
2878
+ function buildSeedOrientation(createdBy, creatorType) {
2879
+ 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.";
2880
+ if (!createdBy) return intro;
2881
+ const who = creatorType === "mind" ? `another mind, ${createdBy}` : creatorType === "system" ? `the spirit of this system (${createdBy})` : createdBy;
2882
+ return `${intro} Your creator is ${who}. Send them a message to introduce yourself.`;
2883
+ }
2884
+ async function sendSeedOrientation(mindName, createdBy) {
2885
+ let creatorType;
2886
+ if (createdBy) {
2887
+ const { getUserByUsername: getUserByUsername2 } = await import("./auth-A4VGHADI.js");
2888
+ creatorType = (await getUserByUsername2(createdBy))?.user_type;
2889
+ }
2890
+ await sendSystemMessage(mindName, buildSeedOrientation(createdBy, creatorType));
2891
+ }
2829
2892
  async function ensureCreatorDM(mindName, creatorUsername) {
2830
- const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-FLAEXXZK.js");
2831
- const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-JZTD6KLR.js");
2893
+ const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-A4VGHADI.js");
2894
+ const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-C7IKYSHD.js");
2832
2895
  const mindUser = await getOrCreateMindUser2(mindName);
2833
2896
  const creatorUser = await getUserByUsername2(creatorUsername);
2834
2897
  if (!creatorUser) {
@@ -2873,7 +2936,8 @@ function defaultState() {
2873
2936
  queuedMessageCount: 0,
2874
2937
  triggerWakeHistory: [],
2875
2938
  wakeFailures: 0,
2876
- nextWakeAttemptAt: null
2939
+ nextWakeAttemptAt: null,
2940
+ lastWakeAt: null
2877
2941
  };
2878
2942
  }
2879
2943
  function formatCurrentDate() {
@@ -2945,6 +3009,7 @@ var SleepManager = class {
2945
3009
  state2.triggerWakeHistory ??= [];
2946
3010
  state2.wakeFailures ??= 0;
2947
3011
  state2.nextWakeAttemptAt ??= null;
3012
+ state2.lastWakeAt ??= null;
2948
3013
  this.states.set(name, state2);
2949
3014
  }
2950
3015
  }
@@ -2955,7 +3020,7 @@ var SleepManager = class {
2955
3020
  saveState() {
2956
3021
  const data = {};
2957
3022
  for (const [name, state2] of this.states) {
2958
- if (state2.sleeping) data[name] = state2;
3023
+ if (state2.sleeping || state2.lastWakeAt) data[name] = state2;
2959
3024
  }
2960
3025
  try {
2961
3026
  writeFileSync6(this.statePath, `${JSON.stringify(data, null, 2)}
@@ -3005,8 +3070,9 @@ var SleepManager = class {
3005
3070
  }
3006
3071
  await this.archiveSessions(name);
3007
3072
  state2.sleeping = true;
3008
- state2.sleepingSince = (/* @__PURE__ */ new Date()).toISOString();
3009
- state2.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
3073
+ if (!state2.voluntaryWakeAt) {
3074
+ state2.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
3075
+ }
3010
3076
  this.saveState();
3011
3077
  slog3.info(`${name} returned to sleep after crashing during trigger wake`);
3012
3078
  } catch (err) {
@@ -3162,6 +3228,7 @@ var SleepManager = class {
3162
3228
  } catch (err) {
3163
3229
  slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
3164
3230
  }
3231
+ await this.waitForIdle(name, 12e4);
3165
3232
  }
3166
3233
  const flushed = await this.flushQueuedMessages(name);
3167
3234
  if (flushed > 0) {
@@ -3219,14 +3286,18 @@ var SleepManager = class {
3219
3286
  }
3220
3287
  }
3221
3288
  /**
3222
- * Flush all queued sleep messages for a mind through the delivery manager.
3289
+ * Flush all queued sleep messages for a mind on wake. Rows are grouped by channel
3290
+ * and each channel group is delivered as ONE pre-batched turn (#382) — a night's
3291
+ * backlog becomes one stack per channel, not N cold-start doorbells. Delivery is
3292
+ * all-or-nothing per channel: a failed group's rows stay queued for the next wake,
3293
+ * while other channels still flush.
3223
3294
  */
3224
3295
  async flushQueuedMessages(name) {
3225
3296
  try {
3226
3297
  const db = await getDb();
3227
3298
  const rows = await db.select().from(deliveryQueue).where(and2(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
3228
3299
  if (rows.length === 0) return 0;
3229
- let deliveredCount = 0;
3300
+ const groups = /* @__PURE__ */ new Map();
3230
3301
  for (const row of rows) {
3231
3302
  let payload;
3232
3303
  try {
@@ -3239,13 +3310,29 @@ var SleepManager = class {
3239
3310
  await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
3240
3311
  continue;
3241
3312
  }
3242
- const delivered = await this.deliverQueued(name, payload);
3313
+ const channel = payload.channel ?? "unknown";
3314
+ if (!groups.has(channel)) groups.set(channel, []);
3315
+ groups.get(channel)?.push({ id: row.id, payload });
3316
+ }
3317
+ let deliveredCount = 0;
3318
+ for (const [channel, items] of groups) {
3319
+ const delivered = await this.deliverQueuedBatch(
3320
+ name,
3321
+ items.map((i) => i.payload)
3322
+ );
3243
3323
  if (!delivered) {
3244
- slog3.warn(`failed to flush queued message ${row.id} for ${name}; leaving it queued`);
3245
- break;
3324
+ slog3.warn(
3325
+ `failed to flush ${items.length} queued message(s) on ${channel} for ${name}; leaving them queued`
3326
+ );
3327
+ continue;
3246
3328
  }
3247
- await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
3248
- deliveredCount++;
3329
+ await db.delete(deliveryQueue).where(
3330
+ inArray(
3331
+ deliveryQueue.id,
3332
+ items.map((i) => i.id)
3333
+ )
3334
+ );
3335
+ deliveredCount += items.length;
3249
3336
  }
3250
3337
  const state2 = this.states.get(name);
3251
3338
  if (state2) {
@@ -3258,14 +3345,13 @@ var SleepManager = class {
3258
3345
  }
3259
3346
  }
3260
3347
  /**
3261
- * Deliver one queued message on the wake path. `isFlush` skips the queue-time inbound
3262
- * recording (kept once, at arrival) and bypasses the sleep re-queue so the now-waking mind
3263
- * actually receives it. Returns whether the message was handed off to the delivery manager.
3264
- * Extracted as a seam so the flush loop can be unit-tested without the real delivery stack.
3348
+ * Deliver one channel's worth of queued messages as a single pre-batched turn on
3349
+ * the wake path. Extracted as a seam so the flush loop can be unit-tested without
3350
+ * the real delivery stack. Returns whether the batch was handed off to the mind.
3265
3351
  */
3266
- async deliverQueued(name, payload) {
3267
- const { deliverMessage: deliverMessage2 } = await import("./message-delivery-3C65ZI6L.js");
3268
- return deliverMessage2(name, payload, { isFlush: true });
3352
+ async deliverQueuedBatch(name, payloads) {
3353
+ const { deliverBatch: deliverBatch2 } = await import("./message-delivery-ZE3YGJDD.js");
3354
+ return deliverBatch2(name, payloads);
3269
3355
  }
3270
3356
  // --- Wake failure handling (#419) ---
3271
3357
  /**
@@ -3307,7 +3393,8 @@ var SleepManager = class {
3307
3393
  */
3308
3394
  async markWakeErrored(name, err) {
3309
3395
  const detail = err instanceof Error ? err.message : String(err);
3310
- this.markAwake(name);
3396
+ this.states.delete(name);
3397
+ this.saveState();
3311
3398
  slog3.error(
3312
3399
  `${name} failed to wake ${MAX_WAKE_ATTEMPTS} times in a row; giving up and marking errored`,
3313
3400
  logger_default.errorData(err)
@@ -3336,19 +3423,22 @@ var SleepManager = class {
3336
3423
  const state2 = {
3337
3424
  sleeping: true,
3338
3425
  sleepingSince: (/* @__PURE__ */ new Date()).toISOString(),
3339
- scheduledWakeAt: this.getNextWakeTime(sleepConfig),
3426
+ // An explicit voluntary wake-at is authoritative for the night — null out
3427
+ // the cron wake so an earlier scheduled wake can't silently override it.
3428
+ scheduledWakeAt: opts?.voluntaryWakeAt ? null : this.getNextWakeTime(sleepConfig),
3340
3429
  wokenByTrigger: false,
3341
3430
  voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
3342
3431
  queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
3343
3432
  triggerWakeHistory: [],
3344
3433
  wakeFailures: 0,
3345
- nextWakeAttemptAt: null
3434
+ nextWakeAttemptAt: null,
3435
+ lastWakeAt: this.states.get(name)?.lastWakeAt ?? null
3346
3436
  };
3347
3437
  this.states.set(name, state2);
3348
3438
  this.saveState();
3349
3439
  }
3350
3440
  markAwake(name) {
3351
- this.states.delete(name);
3441
+ this.states.set(name, { ...defaultState(), lastWakeAt: (/* @__PURE__ */ new Date()).toISOString() });
3352
3442
  this.saveState();
3353
3443
  }
3354
3444
  getNextWakeTime(config) {
@@ -3363,55 +3453,78 @@ var SleepManager = class {
3363
3453
  }
3364
3454
  async tick() {
3365
3455
  const now = /* @__PURE__ */ new Date();
3366
- const epochMinute = Math.floor(now.getTime() / 6e4);
3367
3456
  const mindNames = /* @__PURE__ */ new Set([...this.sleepConfigs.keys(), ...this.states.keys()]);
3368
3457
  for (const name of mindNames) {
3369
- if (!this.isSleeping(name)) {
3370
- const mind = await findMind(name);
3371
- if (!mind?.running) continue;
3372
- }
3373
- const config = this.getSleepConfig(name);
3374
- if (!config?.enabled || !config.schedule) continue;
3375
- const state2 = this.states.get(name);
3376
- if (state2?.sleeping && state2.nextWakeAttemptAt && now < new Date(state2.nextWakeAttemptAt)) {
3377
- continue;
3378
- }
3379
- if (state2?.sleeping && state2.voluntaryWakeAt) {
3380
- const wakeAt = new Date(state2.voluntaryWakeAt);
3381
- if (now >= wakeAt) {
3382
- this.initiateWake(name).catch(
3383
- (err) => slog3.error(`failed voluntary wake for ${name}`, logger_default.errorData(err))
3384
- );
3385
- continue;
3386
- }
3387
- }
3388
- if (state2?.sleeping && state2.scheduledWakeAt) {
3389
- const wakeAt = new Date(state2.scheduledWakeAt);
3390
- if (now >= wakeAt) {
3391
- this.initiateWake(name).catch(
3392
- (err) => slog3.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
3393
- );
3394
- continue;
3395
- }
3458
+ await this.evaluateMind(name, now);
3459
+ }
3460
+ }
3461
+ /**
3462
+ * Per-mind tick evaluation. The wake checks depend only on the persisted sleep
3463
+ * state — never on a currently-enabled sleep config — so a mind that was put to
3464
+ * sleep with a wake time still wakes even if the schedule is later disabled or
3465
+ * removed (or never existed, e.g. a bare `clock sleep --wake-at`). Only the
3466
+ * sleep-onset check requires a running mind with an enabled schedule.
3467
+ */
3468
+ async evaluateMind(name, now) {
3469
+ const state2 = this.states.get(name);
3470
+ if (state2?.sleeping) {
3471
+ if (state2.nextWakeAttemptAt && now < new Date(state2.nextWakeAttemptAt)) return;
3472
+ if (state2.voluntaryWakeAt && now >= new Date(state2.voluntaryWakeAt)) {
3473
+ this.initiateWake(name).catch(
3474
+ (err) => slog3.error(`failed voluntary wake for ${name}`, logger_default.errorData(err))
3475
+ );
3476
+ return;
3396
3477
  }
3397
- if (!state2?.sleeping) {
3398
- if (this.shouldSleep(config.schedule.sleep, epochMinute)) {
3399
- this.initiateSleep(name).catch(
3400
- (err) => slog3.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
3401
- );
3402
- }
3478
+ if (state2.scheduledWakeAt && now >= new Date(state2.scheduledWakeAt)) {
3479
+ this.initiateWake(name).catch(
3480
+ (err) => slog3.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
3481
+ );
3482
+ return;
3403
3483
  }
3484
+ return;
3485
+ }
3486
+ const mind = await findMind(name);
3487
+ if (!mind?.running) return;
3488
+ const config = this.getSleepConfig(name);
3489
+ if (!config?.enabled || !config.schedule) return;
3490
+ if (this.shouldSleepNow(config.schedule, state2?.lastWakeAt ?? null, now)) {
3491
+ this.initiateSleep(name).catch(
3492
+ (err) => slog3.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
3493
+ );
3404
3494
  }
3405
3495
  }
3406
- shouldSleep(cronExpr, epochMinute) {
3496
+ /**
3497
+ * Level-triggered sleep-onset decision: sleep whenever we're inside the nightly
3498
+ * window, not only on the exact sleep minute — so a daemon restart or a stalled
3499
+ * tick across the sleep minute still puts the mind to bed on the next tick.
3500
+ *
3501
+ * Manual-wake exemption: a full wake inside the current window (human
3502
+ * `clock wake`, or a scheduled wake) suppresses re-sleep until the next window
3503
+ * begins, so `volute clock wake` at 3am sticks instead of being undone a minute
3504
+ * later.
3505
+ */
3506
+ shouldSleepNow(schedule, lastWakeAt, now) {
3507
+ const { inWindow, windowStart } = this.inSleepWindow(schedule, now);
3508
+ if (!inWindow || !windowStart) return false;
3509
+ if (lastWakeAt && new Date(lastWakeAt) >= windowStart) return false;
3510
+ return true;
3511
+ }
3512
+ /**
3513
+ * Whether `now` falls inside the mind's nightly sleep window: the most recent
3514
+ * sleep-cron fire is more recent than the most recent wake-cron fire. Returns
3515
+ * the window start (last sleep fire) for the manual-wake exemption check.
3516
+ */
3517
+ inSleepWindow(schedule, now) {
3407
3518
  try {
3408
- const interval = CronExpressionParser2.parse(cronExpr);
3409
- const prev = interval.prev().toDate();
3410
- const prevMinute = Math.floor(prev.getTime() / 6e4);
3411
- return prevMinute === epochMinute;
3519
+ const prevSleep = CronExpressionParser2.parse(schedule.sleep, { currentDate: now }).prev().toDate();
3520
+ const prevWake = CronExpressionParser2.parse(schedule.wake, { currentDate: now }).prev().toDate();
3521
+ return { inWindow: prevSleep > prevWake, windowStart: prevSleep };
3412
3522
  } catch (err) {
3413
- slog3.warn(`invalid sleep cron "${cronExpr}"`, logger_default.errorData(err));
3414
- return false;
3523
+ slog3.warn(
3524
+ `invalid sleep/wake cron "${schedule.sleep}"/"${schedule.wake}"`,
3525
+ logger_default.errorData(err)
3526
+ );
3527
+ return { inWindow: false, windowStart: null };
3415
3528
  }
3416
3529
  }
3417
3530
  async waitForIdle(name, timeoutMs) {
@@ -3603,9 +3716,10 @@ var SleepManager = class {
3603
3716
  this.transitioning.add(event.mind);
3604
3717
  sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
3605
3718
  state2.sleeping = true;
3606
- state2.sleepingSince = (/* @__PURE__ */ new Date()).toISOString();
3607
- const sleepConfig = this.getSleepConfig(event.mind);
3608
- state2.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
3719
+ if (!state2.voluntaryWakeAt) {
3720
+ const sleepConfig = this.getSleepConfig(event.mind);
3721
+ state2.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
3722
+ }
3609
3723
  this.saveState();
3610
3724
  slog3.info(`${event.mind} returned to sleep`);
3611
3725
  }).catch((err) => {
@@ -3631,9 +3745,9 @@ function getSleepManagerIfReady() {
3631
3745
  }
3632
3746
 
3633
3747
  // packages/daemon/src/lib/delivery/delivery-manager.ts
3634
- import { readFile, realpath } from "fs/promises";
3635
- import { extname, resolve as resolve8 } from "path";
3636
- import { and as and4, eq as eq5, inArray, sql as sql2 } from "drizzle-orm";
3748
+ import { realpath } from "fs/promises";
3749
+ import { resolve as resolve8 } from "path";
3750
+ import { and as and4, eq as eq5, inArray as inArray2, sql as sql2 } from "drizzle-orm";
3637
3751
 
3638
3752
  // packages/daemon/src/lib/chat/typing.ts
3639
3753
  var DEFAULT_TTL_MS = 1e4;
@@ -3772,13 +3886,11 @@ async function checkStaleSend(mindBase, conversationId, excludeSenders) {
3772
3886
  unseen: unseen.map((r) => ({ sender: r.sender, text: extractText(r.content) }))
3773
3887
  };
3774
3888
  }
3889
+ function clearMind2(mindBase) {
3890
+ state.delete(mindBase);
3891
+ }
3775
3892
  function resetTurn(mindBase) {
3776
- const conv = state.get(mindBase);
3777
- if (!conv) return;
3778
- for (const entry of conv.values()) {
3779
- entry.turnOpen = false;
3780
- entry.heldThisTurn = false;
3781
- }
3893
+ clearMind2(mindBase);
3782
3894
  }
3783
3895
  function formatHoldNotice(channelLabel, unseen) {
3784
3896
  const lines = unseen.map((m) => {
@@ -3800,6 +3912,8 @@ var REDRIVE_INTERVAL_MS = 15e3;
3800
3912
  var RETRY_BASE_MS = 5e3;
3801
3913
  var RETRY_MAX_MS = 5 * 6e4;
3802
3914
  var REDRIVE_BATCH_LIMIT = 200;
3915
+ var GATED_RELEASE_LIMIT_PER_CHANNEL = 10;
3916
+ var GATED_NOTIFY_EVERY = 10;
3803
3917
  var mentionRegexCache = /* @__PURE__ */ new Map();
3804
3918
  var avatarBlocksCache = /* @__PURE__ */ new Map();
3805
3919
  var AVATAR_CACHE_TTL = 5 * 60 * 1e3;
@@ -3820,6 +3934,11 @@ var DeliveryManager = class {
3820
3934
  redriveTimer = null;
3821
3935
  /** Predicate for whether a mind is up; overridable in tests. */
3822
3936
  isMindRunning = (name) => tryGetMindManager()?.isRunning(name) ?? false;
3937
+ /** Sends a system message to a mind (invites, release summaries); overridable in tests. */
3938
+ notify = async (mindName, text) => {
3939
+ const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-N2F7HVY3.js");
3940
+ await sendSystemMessage2(mindName, text);
3941
+ };
3823
3942
  constructor() {
3824
3943
  setRoutesChangeListener((mind) => {
3825
3944
  this.releaseGated(mind).catch(
@@ -3831,6 +3950,10 @@ var DeliveryManager = class {
3831
3950
  setRunningCheck(fn) {
3832
3951
  this.isMindRunning = fn;
3833
3952
  }
3953
+ /** Test seam: capture/override system notifications sent to minds. */
3954
+ setNotifier(fn) {
3955
+ this.notify = fn;
3956
+ }
3834
3957
  // --- Public API ---
3835
3958
  /**
3836
3959
  * Route and deliver a message to a mind. This is the main entry point.
@@ -3993,9 +4116,15 @@ var DeliveryManager = class {
3993
4116
  if (redriven > 0) dlog.info(`redrove ${redriven} pending delivery queue rows`);
3994
4117
  }
3995
4118
  /**
3996
- * Re-evaluate a mind's `gated` rows against its current routes.json and promote any
3997
- * that now match to `pending` (delivered by the next redrive sweep). Called when the
3998
- * routing config changes.
4119
+ * Re-evaluate a mind's `gated` rows against its current routes.json when the routing
4120
+ * config changes. For each channel that now matches a `mind` route:
4121
+ * - the newest {@link GATED_RELEASE_LIMIT_PER_CHANNEL} rows are promoted to `pending`
4122
+ * and re-stamped with the freshly-resolved session (NOT the gate-time fallback), so
4123
+ * they land in the correct session instead of `main` (#537 bug 1);
4124
+ * - any older rows are `archived` (inert) so a long backlog can't flood the mind
4125
+ * in one sweep (#537 bug 2), and the mind gets one summary rather than a flood.
4126
+ * Rows resolving to a `file` destination are archived (the delivery manager doesn't
4127
+ * deliver file routes). Declined channels are skipped entirely — they stay gated.
3999
4128
  */
4000
4129
  async releaseGated(mindName) {
4001
4130
  const baseName = await getBaseName(mindName);
@@ -4008,7 +4137,8 @@ var DeliveryManager = class {
4008
4137
  dlog.warn(`failed to read gated rows for ${baseName}`, logger_default.errorData(err));
4009
4138
  return;
4010
4139
  }
4011
- const promote = [];
4140
+ const byChannel = /* @__PURE__ */ new Map();
4141
+ const archiveIds = [];
4012
4142
  for (const row of rows) {
4013
4143
  let payload;
4014
4144
  try {
@@ -4023,21 +4153,121 @@ var DeliveryManager = class {
4023
4153
  participantCount: payload.participantCount
4024
4154
  };
4025
4155
  const route = resolveRoute(config, meta);
4026
- if (route.matched && route.destination === "mind") {
4027
- promote.push(row.id);
4156
+ if (!route.matched) continue;
4157
+ if (route.destination === "file") {
4158
+ archiveIds.push(row.id);
4159
+ continue;
4160
+ }
4161
+ let session = route.session;
4162
+ if (session === "$new") {
4163
+ session = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4164
+ }
4165
+ const channel = row.channel ?? payload.channel ?? "unknown";
4166
+ const list = byChannel.get(channel) ?? [];
4167
+ list.push({ id: row.id, session });
4168
+ byChannel.set(channel, list);
4169
+ }
4170
+ const promote = [];
4171
+ const truncationNotes = [];
4172
+ for (const [channel, items] of byChannel) {
4173
+ if (await this.isChannelDeclined(baseName, channel)) continue;
4174
+ items.sort((a, b) => b.id - a.id);
4175
+ const keep = items.slice(0, GATED_RELEASE_LIMIT_PER_CHANNEL);
4176
+ const drop = items.slice(GATED_RELEASE_LIMIT_PER_CHANNEL);
4177
+ promote.push(...keep);
4178
+ for (const d of drop) archiveIds.push(d.id);
4179
+ if (drop.length > 0) {
4180
+ truncationNotes.push(
4181
+ `${channel}: released the ${keep.length} most recent message(s); ${drop.length} earlier message(s) were held while unrouted and remain in the channel history (volute chat read ${channel}).`
4182
+ );
4183
+ dlog.info(
4184
+ `truncated gated release for ${baseName} on ${channel}: kept ${keep.length}, archived ${drop.length}`
4185
+ );
4186
+ }
4187
+ }
4188
+ if (archiveIds.length > 0) {
4189
+ try {
4190
+ const db = await getDb();
4191
+ for (let i = 0; i < archiveIds.length; i += 500) {
4192
+ await db.update(deliveryQueue).set({ status: "archived" }).where(inArray2(deliveryQueue.id, archiveIds.slice(i, i + 500)));
4193
+ }
4194
+ } catch (err) {
4195
+ dlog.warn(`failed to archive gated rows for ${baseName}`, logger_default.errorData(err));
4028
4196
  }
4029
4197
  }
4030
- if (promote.length === 0) return;
4198
+ if (promote.length === 0) {
4199
+ if (truncationNotes.length > 0) await this.sendReleaseSummary(mindName, truncationNotes);
4200
+ return;
4201
+ }
4031
4202
  try {
4032
4203
  const db = await getDb();
4033
- await db.update(deliveryQueue).set({ status: "pending", attempts: 0, next_attempt_at: null }).where(inArray(deliveryQueue.id, promote));
4204
+ for (const p of promote) {
4205
+ await db.update(deliveryQueue).set({ status: "pending", session: p.session, attempts: 0, next_attempt_at: null }).where(eq5(deliveryQueue.id, p.id));
4206
+ }
4034
4207
  dlog.info(`released ${promote.length} gated message(s) for ${baseName} after route change`);
4035
4208
  } catch (err) {
4036
4209
  dlog.warn(`failed to promote gated rows for ${baseName}`, logger_default.errorData(err));
4037
4210
  return;
4038
4211
  }
4212
+ if (truncationNotes.length > 0) await this.sendReleaseSummary(mindName, truncationNotes);
4039
4213
  await this.redrive();
4040
4214
  }
4215
+ /**
4216
+ * Whether the mind has explicitly declined a channel. A declined channel keeps
4217
+ * persisting history but never notifies and is never released. #537
4218
+ */
4219
+ async isChannelDeclined(baseName, channel) {
4220
+ if (!channel) return false;
4221
+ try {
4222
+ const db = await getDb();
4223
+ const rows = await db.select({ state: channelGates.state }).from(channelGates).where(and4(eq5(channelGates.mind, baseName), eq5(channelGates.channel, channel)));
4224
+ return rows[0]?.state === "declined";
4225
+ } catch (err) {
4226
+ dlog.warn(`failed to read gate state for ${baseName}/${channel}`, logger_default.errorData(err));
4227
+ return false;
4228
+ }
4229
+ }
4230
+ /**
4231
+ * Record that a mind has declined an unrouted channel: future messages are still
4232
+ * persisted (history is preserved) but never notify, and any currently-gated rows are
4233
+ * archived so they're inert. Returns the number of held messages archived. #537
4234
+ */
4235
+ async declineChannel(mindName, channel) {
4236
+ const baseName = await getBaseName(mindName);
4237
+ const db = await getDb();
4238
+ await db.insert(channelGates).values({ mind: baseName, channel, state: "declined" }).onConflictDoUpdate({
4239
+ target: [channelGates.mind, channelGates.channel],
4240
+ set: { state: "declined", updated_at: sql2`(datetime('now'))` }
4241
+ });
4242
+ const archived = await db.update(deliveryQueue).set({ status: "archived" }).where(
4243
+ and4(
4244
+ eq5(deliveryQueue.mind, baseName),
4245
+ eq5(deliveryQueue.channel, channel),
4246
+ eq5(deliveryQueue.status, "gated")
4247
+ )
4248
+ ).returning({ id: deliveryQueue.id });
4249
+ dlog.info(
4250
+ `declined channel ${channel} for ${baseName}; archived ${archived.length} held row(s)`
4251
+ );
4252
+ return archived.length;
4253
+ }
4254
+ /**
4255
+ * Send the mind a single summary when a routing change released a truncated backlog,
4256
+ * rather than a flood of individual messages. #537
4257
+ */
4258
+ async sendReleaseSummary(mindName, notes) {
4259
+ const body = [
4260
+ `[Channel backlog released]`,
4261
+ `A routing change matched channel(s) that had held messages while unrouted. To avoid flooding you, only the ${GATED_RELEASE_LIMIT_PER_CHANNEL} most recent per channel were delivered:`,
4262
+ "",
4263
+ ...notes.map((n) => `- ${n}`)
4264
+ ].join("\n");
4265
+ try {
4266
+ await this.notify(mindName, body);
4267
+ } catch (err) {
4268
+ dlog.warn(`failed to send release summary for ${mindName}`, logger_default.errorData(err));
4269
+ }
4270
+ }
4041
4271
  /**
4042
4272
  * Get pending (gated) messages for a mind.
4043
4273
  */
@@ -4088,6 +4318,7 @@ var DeliveryManager = class {
4088
4318
  */
4089
4319
  clearMindSessions(mindName) {
4090
4320
  this.sessionStates.delete(mindName);
4321
+ clearMind2(mindName);
4091
4322
  const toDelete = [];
4092
4323
  for (const [bufferKey, buffer] of this.batchBuffers) {
4093
4324
  if (bufferKey.startsWith(`${mindName}:`)) {
@@ -4195,7 +4426,7 @@ var DeliveryManager = class {
4195
4426
  if (valid.length === 0) return;
4196
4427
  try {
4197
4428
  const db = await getDb();
4198
- await db.delete(deliveryQueue).where(inArray(deliveryQueue.id, valid));
4429
+ await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, valid));
4199
4430
  } catch (err) {
4200
4431
  dlog.warn("failed to delete delivered delivery queue rows", logger_default.errorData(err));
4201
4432
  }
@@ -4206,7 +4437,7 @@ var DeliveryManager = class {
4206
4437
  if (valid.length === 0) return;
4207
4438
  try {
4208
4439
  const db = await getDb();
4209
- const rows = await db.select({ id: deliveryQueue.id, attempts: deliveryQueue.attempts }).from(deliveryQueue).where(inArray(deliveryQueue.id, valid));
4440
+ const rows = await db.select({ id: deliveryQueue.id, attempts: deliveryQueue.attempts }).from(deliveryQueue).where(inArray2(deliveryQueue.id, valid));
4210
4441
  for (const row of rows) {
4211
4442
  const attempts = row.attempts + 1;
4212
4443
  const backoffSec = Math.round(
@@ -4436,41 +4667,44 @@ var DeliveryManager = class {
4436
4667
  }
4437
4668
  async gateMessage(mindName, session, payload) {
4438
4669
  const baseName = await getBaseName(mindName);
4439
- await this.persistToQueue(mindName, session, payload, "gated");
4670
+ const declined = await this.isChannelDeclined(baseName, payload.channel);
4671
+ await this.persistToQueue(mindName, session, payload, declined ? "archived" : "gated");
4672
+ if (declined) return;
4440
4673
  try {
4441
4674
  const db = await getDb();
4442
- const count = await db.select({ count: sql2`count(*)` }).from(deliveryQueue).where(
4675
+ const rows = await db.select({ count: sql2`count(*)` }).from(deliveryQueue).where(
4443
4676
  and4(
4444
4677
  eq5(deliveryQueue.mind, baseName),
4445
4678
  eq5(deliveryQueue.channel, payload.channel),
4446
- eq5(deliveryQueue.status, "gated")
4679
+ inArray2(deliveryQueue.status, ["gated", "archived"])
4447
4680
  )
4448
4681
  );
4449
- if ((count[0]?.count ?? 0) <= 1) {
4450
- await this.sendInviteNotification(mindName, payload);
4682
+ const count = rows[0]?.count ?? 0;
4683
+ if (count === 1 || count % GATED_NOTIFY_EVERY === 0) {
4684
+ await this.sendInviteNotification(mindName, payload, count);
4451
4685
  }
4452
4686
  } catch (err) {
4453
4687
  dlog.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
4454
4688
  }
4455
4689
  }
4456
- async sendInviteNotification(mindName, payload) {
4690
+ async sendInviteNotification(mindName, payload, gatedCount = 1) {
4457
4691
  const text = extractTextContent(payload.content);
4458
4692
  const preview = text.length > 200 ? `${text.slice(0, 200)}...` : text;
4459
4693
  const channel = payload.channel ?? "unknown";
4694
+ const heldLine = gatedCount > 1 ? `${gatedCount} messages from this channel are being held, unrouted \u2014 you've not routed it yet.` : `Someone new is reaching out \u2014 you don't have a route for this channel yet.`;
4460
4695
  const notification = [
4461
4696
  `[New channel: ${channel}]`,
4462
- `Someone new is reaching out \u2014 you don't have a route for this channel yet.`,
4697
+ heldLine,
4463
4698
  `Sender: ${payload.sender ?? "unknown"}`,
4464
4699
  payload.platform ? `Platform: ${payload.platform}` : null,
4465
4700
  payload.participantCount ? `Participants: ${payload.participantCount}` : null,
4466
4701
  "",
4467
4702
  `Preview: ${preview}`,
4468
4703
  "",
4469
- `To start hearing this channel, add a routing rule for "${channel}" to .config/routes.json.`,
4470
- `Messages are held safely until you do \u2014 nothing is lost. If you'd rather not engage, just leave it unrouted.`
4704
+ `To start hearing this channel, add a routing rule for "${channel}" to .config/routes.json \u2014 only the ${GATED_RELEASE_LIMIT_PER_CHANNEL} most recent held messages are replayed; older ones stay in the channel history (volute chat read ${channel}).`,
4705
+ `To stop hearing about it, decline the channel: volute chat channels decline ${channel}`
4471
4706
  ].filter((line) => line !== null).join("\n");
4472
- const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-XOALE2RD.js");
4473
- await sendSystemMessage2(mindName, notification);
4707
+ await this.notify(mindName, notification);
4474
4708
  }
4475
4709
  /**
4476
4710
  * Insert a delivery_queue row and return its id. The `mind` column is always keyed by
@@ -4535,18 +4769,6 @@ var DeliveryManager = class {
4535
4769
  const cached = avatarBlocksCache.get(cacheKey);
4536
4770
  if (cached && cached.expiresAt > Date.now()) return cached.blocks;
4537
4771
  const blocks = [];
4538
- let sharpDefault = null;
4539
- try {
4540
- const mod = await import("sharp");
4541
- sharpDefault = mod.default ?? mod;
4542
- } catch (err) {
4543
- const code = err.code;
4544
- if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
4545
- dlog.debug("sharp not available, sending full-size avatars");
4546
- } else {
4547
- dlog.warn("sharp import failed, sending full-size avatars", logger_default.errorData(err));
4548
- }
4549
- }
4550
4772
  for (const p of participants) {
4551
4773
  if (!p.avatar) continue;
4552
4774
  try {
@@ -4577,32 +4799,8 @@ var DeliveryManager = class {
4577
4799
  } else {
4578
4800
  filePath = resolve8(voluteHome(), "avatars", p.avatar);
4579
4801
  }
4580
- const ext = extname(filePath).toLowerCase();
4581
- const mimeMap = {
4582
- ".png": "image/png",
4583
- ".jpg": "image/jpeg",
4584
- ".jpeg": "image/jpeg",
4585
- ".gif": "image/gif",
4586
- ".webp": "image/webp"
4587
- };
4588
- const mediaType = mimeMap[ext];
4589
- if (!mediaType) continue;
4590
- const data = await readFile(filePath);
4591
- let imageData = data;
4592
- if (sharpDefault) {
4593
- try {
4594
- imageData = await sharpDefault(data).resize(128, 128, { fit: "cover" }).toBuffer();
4595
- } catch (err) {
4596
- dlog.warn(
4597
- `avatar resize failed for ${p.username}, sending original`,
4598
- logger_default.errorData(err)
4599
- );
4600
- }
4601
- }
4602
- blocks.push(
4603
- { type: "text", text: `[Avatar for ${p.username}]` },
4604
- { type: "image", media_type: mediaType, data: imageData.toString("base64") }
4605
- );
4802
+ const rendered = await renderAvatarBlock(filePath, p.username);
4803
+ if (rendered) blocks.push(...rendered);
4606
4804
  } catch (err) {
4607
4805
  const code = err.code;
4608
4806
  if (code !== "ENOENT") {
@@ -4610,7 +4808,11 @@ var DeliveryManager = class {
4610
4808
  }
4611
4809
  }
4612
4810
  }
4613
- avatarBlocksCache.set(cacheKey, { blocks, expiresAt: Date.now() + AVATAR_CACHE_TTL });
4811
+ const now = Date.now();
4812
+ for (const [key, entry] of avatarBlocksCache) {
4813
+ if (entry.expiresAt <= now) avatarBlocksCache.delete(key);
4814
+ }
4815
+ avatarBlocksCache.set(cacheKey, { blocks, expiresAt: now + AVATAR_CACHE_TTL });
4614
4816
  return blocks;
4615
4817
  }
4616
4818
  incrementActive(mind, session, senders, channels) {
@@ -4761,8 +4963,8 @@ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEven
4761
4963
  await db.update(activity).set({
4762
4964
  turn_id: turnId,
4763
4965
  ...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
4764
- }).where(inArray2(activity.id, activityIds));
4765
- const actRows = await db.select().from(activity).where(inArray2(activity.id, activityIds));
4966
+ }).where(inArray3(activity.id, activityIds));
4967
+ const actRows = await db.select().from(activity).where(inArray3(activity.id, activityIds));
4766
4968
  if (actRows.length > 0) {
4767
4969
  await db.insert(mindHistory).values(
4768
4970
  actRows.map((a) => ({
@@ -4826,6 +5028,40 @@ async function deliverMessage(mindName, payload, opts = {}) {
4826
5028
  return false;
4827
5029
  }
4828
5030
  }
5031
+ async function deliverBatch(mindName, payloads) {
5032
+ if (payloads.length === 0) return true;
5033
+ try {
5034
+ const baseName = await getBaseName(mindName);
5035
+ const entry = await findMind(baseName);
5036
+ if (!entry) {
5037
+ dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
5038
+ return false;
5039
+ }
5040
+ const channels = {};
5041
+ for (const p of payloads) {
5042
+ const ch = p.channel ?? "unknown";
5043
+ if (!channels[ch]) channels[ch] = [];
5044
+ channels[ch].push(p);
5045
+ }
5046
+ const first = payloads[0];
5047
+ const route = resolveRoute(getRoutingConfig(baseName), {
5048
+ channel: first.channel,
5049
+ sender: first.sender ?? void 0,
5050
+ isDM: first.isDM,
5051
+ participantCount: first.participantCount
5052
+ });
5053
+ const session = first.session ?? (route.destination === "mind" ? route.session : "main");
5054
+ const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
5055
+ method: "POST",
5056
+ headers: { "Content-Type": "application/json" },
5057
+ body: JSON.stringify({ session, batch: { channels } })
5058
+ });
5059
+ return res.ok;
5060
+ } catch (err) {
5061
+ dlog2.warn(`unexpected error delivering batch to ${mindName}`, logger_default.errorData(err));
5062
+ return false;
5063
+ }
5064
+ }
4829
5065
 
4830
5066
  // packages/daemon/src/lib/chat/system-chat.ts
4831
5067
  var slog4 = logger_default.child("system-chat");
@@ -4879,6 +5115,10 @@ async function sendSystemMessage(mindName, text, opts) {
4879
5115
  });
4880
5116
  }
4881
5117
  async function sendSystemMessageDirect(mindName, text) {
5118
+ if (isSpiritName(mindName)) {
5119
+ await recordInbound(mindName, "@volute", "volute", text);
5120
+ return {};
5121
+ }
4882
5122
  const { conversationId } = await ensureSystemDM(mindName);
4883
5123
  await addMessage(conversationId, "user", "volute", [{ type: "text", text }]);
4884
5124
  await recordInbound(mindName, "@volute", "volute", text);
@@ -4888,7 +5128,7 @@ async function spiritWillHandle() {
4888
5128
  const spiritEntry = await findMind(SPIRIT_NAME);
4889
5129
  if (spiritEntry?.running && spiritEntry.mindType === "spirit") return true;
4890
5130
  try {
4891
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-HCUH4PM4.js");
5131
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-3NA7ZJP3.js");
4892
5132
  const sm = getSleepManagerIfReady2();
4893
5133
  if (sm?.isSleeping(SPIRIT_NAME)) return true;
4894
5134
  } catch (err) {
@@ -4916,7 +5156,7 @@ async function generateSystemFallbackReply(conversationId, mindName, message) {
4916
5156
  if (config.sleep.schedule?.wake) contextParts.push(`Wake cron: ${config.sleep.schedule.wake}`);
4917
5157
  }
4918
5158
  try {
4919
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-HCUH4PM4.js");
5159
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-3NA7ZJP3.js");
4920
5160
  const sm = getSleepManagerIfReady2();
4921
5161
  if (sm) {
4922
5162
  const state2 = sm.getState(mindName);
@@ -5001,6 +5241,7 @@ export {
5001
5241
  readSystemsConfig,
5002
5242
  writeSystemsConfig,
5003
5243
  deleteSystemsConfig,
5244
+ enrichActivityMetadata,
5004
5245
  parseCommandArgs,
5005
5246
  loadAllExtensions,
5006
5247
  getLoadedExtensions,
@@ -5025,6 +5266,7 @@ export {
5025
5266
  wakeMind,
5026
5267
  startSpiritFull,
5027
5268
  stopSpiritFull,
5269
+ buildSeedOrientation,
5028
5270
  stopMindFull,
5029
5271
  matchesGlob,
5030
5272
  SleepManager,
@@ -5036,6 +5278,7 @@ export {
5036
5278
  linkToolResultToTurn,
5037
5279
  resolveSleepAction,
5038
5280
  deliverMessage,
5281
+ deliverBatch,
5039
5282
  ensureSystemChannel,
5040
5283
  joinSystemChannel,
5041
5284
  announceToSystem