volute 0.34.0 → 0.35.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 (209) hide show
  1. package/README.md +7 -6
  2. package/dist/accept-ZBDVVCEU.js +42 -0
  3. package/dist/{activity-events-BN7V6KCC.js → activity-events-ZW4SDL2C.js} +4 -4
  4. package/dist/{ai-service-PSILB5WD.js → ai-service-LURBEDDB.js} +5 -5
  5. package/dist/{api-client-XUXOB7LI.js → api-client-3A77HMH7.js} +1 -1
  6. package/dist/api.d.ts +1 -5618
  7. package/dist/{archive-C2VEMQOR.js → archive-ESU2FUN4.js} +3 -3
  8. package/dist/{auth-ZFZXJZDQ.js → auth-WX4TESEI.js} +5 -5
  9. package/dist/bridge-PXIO6PS2.js +206 -0
  10. package/dist/chat-QXAJF3FU.js +51 -0
  11. package/dist/{chunk-7F2SW2KD.js → chunk-2TGZJFAT.js} +3 -3
  12. package/dist/{chunk-6LXAAQ43.js → chunk-33ODGMFZ.js} +1 -1
  13. package/dist/{chunk-4JSR7YO7.js → chunk-5N7Y5WAM.js} +1 -1
  14. package/dist/{chunk-FYCALD4Q.js → chunk-5T5YMX6S.js} +1 -1
  15. package/dist/{chunk-B2BVAIZ4.js → chunk-5XJYUFZH.js} +21 -15
  16. package/dist/{chunk-M3K5AARV.js → chunk-A2ZLHBHG.js} +2 -2
  17. package/dist/{chunk-U5BTYSAL.js → chunk-AN2W47GW.js} +2 -2
  18. package/dist/{chunk-G53F3JA4.js → chunk-AOB6GVRM.js} +1 -1
  19. package/dist/{chunk-N7BLAHNE.js → chunk-BDYXIWA5.js} +5 -5
  20. package/dist/{chunk-YUIHSKR6.js → chunk-BKF4WQCY.js} +2 -2
  21. package/dist/{chunk-6OWJXUAR.js → chunk-BMZQYACC.js} +2 -2
  22. package/dist/{chunk-NAOW2CLO.js → chunk-BTY4WNFE.js} +1 -1
  23. package/dist/{chunk-MLOQKQNB.js → chunk-BV65KRHM.js} +2 -2
  24. package/dist/{chunk-XWXBJQBE.js → chunk-CORXD635.js} +4 -4
  25. package/dist/{chunk-PVY5W6QN.js → chunk-F7ZNLYKZ.js} +2 -2
  26. package/dist/{chunk-BFWHBQK4.js → chunk-FT5KETXZ.js} +3 -3
  27. package/dist/{chunk-N3DNFPVA.js → chunk-IJHIXLVN.js} +8 -8
  28. package/dist/{chunk-V6ZCNULL.js → chunk-J6CJQDWI.js} +37 -28
  29. package/dist/{chunk-4RQBJWQX.js → chunk-LOPXTW6H.js} +1 -1
  30. package/dist/{chunk-47ZPNLF4.js → chunk-MDJGMOSD.js} +8 -137
  31. package/dist/{chunk-BTWAGDV5.js → chunk-N446KRP7.js} +3 -3
  32. package/dist/{chunk-6WAWMWR5.js → chunk-N5LMGYXX.js} +2 -2
  33. package/dist/{chunk-G6BSYHPK.js → chunk-NJK5SDGR.js} +1 -1
  34. package/dist/{chunk-D424ZQGI.js → chunk-O7IGP7ZW.js} +11 -3
  35. package/dist/{chunk-2IOP6PHB.js → chunk-OTC67N2Z.js} +2 -2
  36. package/dist/{chunk-V45JXOWY.js → chunk-PWQ2ITYG.js} +4 -4
  37. package/dist/{chunk-KTLFDYPT.js → chunk-QCH6K235.js} +1 -1
  38. package/dist/chunk-QHG4OMZL.js +145 -0
  39. package/dist/{chunk-IS7WJ56Q.js → chunk-QWTR6AWZ.js} +3 -3
  40. package/dist/chunk-TXSA4Q3V.js +116 -0
  41. package/dist/{chunk-BDK73LK6.js → chunk-VHJRZM2S.js} +2 -2
  42. package/dist/{chunk-SSI47XP2.js → chunk-VHWGEJ4V.js} +1 -1
  43. package/dist/chunk-VY3RB2V7.js +164 -0
  44. package/dist/chunk-WJPROOU5.js +8314 -0
  45. package/dist/{chunk-E5C7OWZ2.js → chunk-WZRZFFCL.js} +8 -8
  46. package/dist/{chunk-BM474GX6.js → chunk-XRQSAMX2.js} +4 -4
  47. package/dist/{chunk-OYAKCAVY.js → chunk-ZSR72JB3.js} +1 -1
  48. package/dist/{chunk-PLDWHR4D.js → chunk-ZX7EAV5J.js} +17 -7
  49. package/dist/cli.js +90 -29
  50. package/dist/clock-HSEKS5AR.js +289 -0
  51. package/dist/{cloud-sync-TG3TIX5H.js → cloud-sync-6JL4C24T.js} +21 -22
  52. package/dist/config-UTS7QULS.js +76 -0
  53. package/dist/connectors/discord-bridge.js +3 -3
  54. package/dist/connectors/slack-bridge.js +3 -3
  55. package/dist/connectors/telegram-bridge.js +3 -3
  56. package/dist/{conversations-HL2JP5GI.js → conversations-2PW57WO2.js} +5 -5
  57. package/dist/create-5BPOOJAN.js +75 -0
  58. package/dist/create-UVCK2CS6.js +50 -0
  59. package/dist/daemon-client-RVIKXGFQ.js +12 -0
  60. package/dist/daemon-restart-HSZ3BCX5.js +65 -0
  61. package/dist/daemon.js +845 -1766
  62. package/dist/{db-PLEDCBHZ.js → db-BDMH4SZ2.js} +7 -3
  63. package/dist/{db-RYX3SS2W.js → db-BVBJ57TU.js} +2 -2
  64. package/dist/delete-L5PAVDGQ.js +42 -0
  65. package/dist/delivery-manager-H5ZVBMCQ.js +31 -0
  66. package/dist/{delivery-router-D5ELDMS2.js → delivery-router-HEJSJAHQ.js} +4 -4
  67. package/dist/down-74VXM45A.js +17 -0
  68. package/dist/env-E4XHO2BI.js +223 -0
  69. package/dist/{exec-DVLXKRIO.js → exec-PY7THYH4.js} +4 -4
  70. package/dist/export-OAS6QVBN.js +113 -0
  71. package/dist/{extension-PM42QCID.js → extension-D74CNM7G.js} +25 -33
  72. package/dist/{extensions-BBGVL5JC.js → extensions-XDDFY72A.js} +22 -11
  73. package/dist/files-CWTK6V3H.js +53 -0
  74. package/dist/import-5A3T7QV4.js +143 -0
  75. package/dist/{isolation-62MKDZN3.js → isolation-TK5RX2WM.js} +3 -3
  76. package/dist/join-DF5XSJAC.js +67 -0
  77. package/dist/list-PDMQM7ZV.js +53 -0
  78. package/dist/login-7TE6CIZF.js +60 -0
  79. package/dist/login-GOTAYLXP.js +51 -0
  80. package/dist/logout-6KIA74EV.js +29 -0
  81. package/dist/logout-T4XS6LRU.js +50 -0
  82. package/dist/message-delivery-GRC4W6P7.js +41 -0
  83. package/dist/mind-5IEYKV7I.js +97 -0
  84. package/dist/{mind-activity-tracker-2ACNHA7B.js → mind-activity-tracker-QBLIV7ZJ.js} +5 -5
  85. package/dist/{mind-history-WOYFLQAI.js → mind-history-IE2QH7U5.js} +82 -71
  86. package/dist/mind-list-GEWHWAL4.js +38 -0
  87. package/dist/mind-manager-HFLB5653.js +31 -0
  88. package/dist/mind-profile-DCBDVF5B.js +53 -0
  89. package/dist/mind-service-X2CAA6W6.js +37 -0
  90. package/dist/mind-sleep-ITCF6OQA.js +47 -0
  91. package/dist/mind-status-X4SX3YUG.js +65 -0
  92. package/dist/mind-wake-KXMKMGWX.js +42 -0
  93. package/dist/{package-V2WHWVG6.js → package-D2FSVFAX.js} +5 -5
  94. package/dist/read-67VRP2DO.js +91 -0
  95. package/dist/{read-stdin-PIRM6A2Y.js → read-stdin-3X5VYKNS.js} +1 -1
  96. package/dist/register-SB7NXCOE.js +51 -0
  97. package/dist/{registry-UYV5S6QT.js → registry-GBSNW3HG.js} +2 -2
  98. package/dist/reject-MUR2KWJ4.js +40 -0
  99. package/dist/restart-5EGG4JXU.js +42 -0
  100. package/dist/{sandbox-SI5HMBP3.js → sandbox-R37VIU36.js} +5 -5
  101. package/dist/scheduler-Y7O4CJXL.js +31 -0
  102. package/dist/{schema-ETMABTW4.js → schema-XVZ2CLKW.js} +1 -1
  103. package/dist/{seed-WNGI6PNW.js → seed-EQORWX77.js} +2 -2
  104. package/dist/seed-check-KJNTL72M.js +35 -0
  105. package/dist/seed-cmd-ZM2XGVU2.js +30 -0
  106. package/dist/seed-create-DRWGGHEI.js +113 -0
  107. package/dist/seed-sprout-JYXGXOP3.js +148 -0
  108. package/dist/send-JBJJQ7CA.js +409 -0
  109. package/dist/service-WNPCNHOX.js +121 -0
  110. package/dist/{setup-Z3DEVWV7.js → setup-BJ4YAY26.js} +153 -127
  111. package/dist/{setup-GGMKENLN.js → setup-RHJRFURI.js} +3 -3
  112. package/dist/skill-TAAKEYBV.js +389 -0
  113. package/dist/skills/volute-mind/SKILL.md +3 -7
  114. package/dist/skills/volute-mind/references/extensions.md +8 -11
  115. package/dist/{skills-Q6VZ2UGD.js → skills-EKMCQ46K.js} +7 -7
  116. package/dist/sleep-manager-7KFK3USC.js +35 -0
  117. package/dist/spirit-ZFRDXMG7.js +23 -0
  118. package/dist/split-AWVOYOPZ.js +64 -0
  119. package/dist/{sprout-E3HJIV2Z.js → sprout-HE4TITMK.js} +2 -2
  120. package/dist/start-3UXOPXQG.js +39 -0
  121. package/dist/status-ZK34WYIM.js +125 -0
  122. package/dist/stop-3XYIBGFM.js +41 -0
  123. package/dist/system-chat-IDPHYHY4.js +35 -0
  124. package/dist/systems-O43WGQY6.js +52 -0
  125. package/dist/{tailscale-ZEUK7GKZ.js → tailscale-ZIZ2HWJ5.js} +4 -4
  126. package/dist/{template-hash-EJRTKE36.js → template-hash-A7FNHTB7.js} +2 -2
  127. package/dist/up-77ICEDEW.js +19 -0
  128. package/dist/update-ANE5ZM7F.js +225 -0
  129. package/dist/{update-check-X3YG4WVP.js → update-check-UV55CBEP.js} +3 -3
  130. package/dist/upgrade-ZMDGC7M2.js +74 -0
  131. package/dist/variant-QWL2WSRI.js +62 -0
  132. package/dist/{version-notify-YCH4UVQ2.js → version-notify-FXSEMXWW.js} +28 -27
  133. package/dist/{volute-config-WBKYJGYQ.js → volute-config-D2XVS2YI.js} +1 -1
  134. package/dist/web-assets/assets/index-BhxWKvbB.css +1 -0
  135. package/dist/web-assets/assets/index-CHVKJ9II.js +75 -0
  136. package/dist/web-assets/index.html +2 -2
  137. package/dist/web-assets/sw.js +117 -0
  138. package/package.json +5 -5
  139. package/packages/extensions/pages/dist/ui/assets/index-DKZLNMED.js +2 -0
  140. package/packages/extensions/pages/dist/ui/index.html +1 -1
  141. package/packages/extensions/pages/skills/pages/SKILL.md +84 -9
  142. package/templates/_base/src/lib/auto-commit.ts +8 -8
  143. package/templates/_base/src/lib/volute-server.ts +6 -0
  144. package/templates/claude/src/agent.ts +8 -1
  145. package/dist/accept-TW6V4WI4.js +0 -42
  146. package/dist/bridge-O753D5F4.js +0 -207
  147. package/dist/chat-BHYX7DJ4.js +0 -68
  148. package/dist/chunk-47XDEWWV.js +0 -156
  149. package/dist/chunk-CVL5IGIR.js +0 -2084
  150. package/dist/chunk-PB65JZK2.js +0 -85
  151. package/dist/chunk-TAHX36HZ.js +0 -3679
  152. package/dist/clock-3X4DSC2N.js +0 -281
  153. package/dist/config-OROA5DUA.js +0 -72
  154. package/dist/create-3SEKKI6P.js +0 -71
  155. package/dist/create-UOSOQ2HN.js +0 -44
  156. package/dist/daemon-client-WOAQXXBM.js +0 -12
  157. package/dist/daemon-restart-5ABHNXJZ.js +0 -52
  158. package/dist/delete-KYOVWR23.js +0 -35
  159. package/dist/delivery-manager-2BR5NZKF.js +0 -32
  160. package/dist/down-QVFN4UPK.js +0 -15
  161. package/dist/env-R34DT7XL.js +0 -195
  162. package/dist/export-6ZXAXATG.js +0 -112
  163. package/dist/files-VQV2VZQO.js +0 -47
  164. package/dist/import-MK2I2T6F.js +0 -23
  165. package/dist/join-DGYHTJUH.js +0 -66
  166. package/dist/list-C644WTHV.js +0 -49
  167. package/dist/login-IIGEQPHL.js +0 -47
  168. package/dist/login-KZQLMAWE.js +0 -47
  169. package/dist/logout-AGTZVRGP.js +0 -40
  170. package/dist/logout-KD6GXIJJ.js +0 -21
  171. package/dist/message-delivery-V3R6NXJP.js +0 -42
  172. package/dist/mind-BI4EPBVZ.js +0 -108
  173. package/dist/mind-list-6VPM7GUQ.js +0 -30
  174. package/dist/mind-manager-MWW3BTS4.js +0 -32
  175. package/dist/mind-profile-WPG42U5Y.js +0 -47
  176. package/dist/mind-service-VIKZJK2M.js +0 -38
  177. package/dist/mind-sleep-XDISJY74.js +0 -42
  178. package/dist/mind-status-7FTZWPZF.js +0 -56
  179. package/dist/mind-wake-KIIKEI3A.js +0 -37
  180. package/dist/read-H5C26YO7.js +0 -85
  181. package/dist/register-J27WP33N.js +0 -47
  182. package/dist/reject-OEANJYIA.js +0 -40
  183. package/dist/restart-V5EGYBJG.js +0 -33
  184. package/dist/scheduler-AGG3L2FO.js +0 -32
  185. package/dist/seed-check-PXTH7YXS.js +0 -32
  186. package/dist/seed-cmd-VENFTGS3.js +0 -36
  187. package/dist/seed-create-663ALOKH.js +0 -112
  188. package/dist/seed-sprout-EH3AGKAI.js +0 -132
  189. package/dist/send-7FUUUZZH.js +0 -386
  190. package/dist/skill-DKNYJS4P.js +0 -362
  191. package/dist/skills/shared-files/SKILL.md +0 -44
  192. package/dist/skills/shared-files/scripts/merge.ts +0 -72
  193. package/dist/skills/shared-files/scripts/pull.ts +0 -52
  194. package/dist/sleep-manager-BJK2ROPX.js +0 -36
  195. package/dist/spirit-4JP4TY4C.js +0 -23
  196. package/dist/split-3YPMS2CL.js +0 -63
  197. package/dist/start-W3TPKX4D.js +0 -33
  198. package/dist/status-4OVFXFEJ.js +0 -115
  199. package/dist/stop-GTT6YWYO.js +0 -32
  200. package/dist/system-channel-DXD2JBOU.js +0 -36
  201. package/dist/system-chat-TYLOL7SX.js +0 -36
  202. package/dist/systems-AYLO727G.js +0 -61
  203. package/dist/up-PA7F2CXE.js +0 -18
  204. package/dist/update-HG4LCUSG.js +0 -215
  205. package/dist/upgrade-YGNIDICG.js +0 -67
  206. package/dist/variant-MZUMRTQO.js +0 -41
  207. package/dist/web-assets/assets/index-DiiwC-CZ.css +0 -1
  208. package/dist/web-assets/assets/index-d6y5b9Ij.js +0 -75
  209. package/packages/extensions/pages/dist/ui/assets/index-tLTROSk5.js +0 -2
@@ -1,3679 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- notifyExtensionsMindStart,
4
- notifyExtensionsMindStop,
5
- readSystemsConfig
6
- } from "./chunk-CVL5IGIR.js";
7
- import {
8
- spiritDir
9
- } from "./chunk-B2BVAIZ4.js";
10
- import {
11
- readVoluteConfig,
12
- writeVoluteConfig
13
- } from "./chunk-OYAKCAVY.js";
14
- import {
15
- isSandboxEnabled,
16
- wrapForSandbox
17
- } from "./chunk-V45JXOWY.js";
18
- import {
19
- extractTextContent,
20
- getRoutingConfig,
21
- resolveDeliveryMode,
22
- resolveRoute
23
- } from "./chunk-IS7WJ56Q.js";
24
- import {
25
- markIdle
26
- } from "./chunk-BTWAGDV5.js";
27
- import {
28
- loadMergedEnv
29
- } from "./chunk-M3K5AARV.js";
30
- import {
31
- getOrCreateMindUser,
32
- getOrCreateSystemUser,
33
- syncMindProfile
34
- } from "./chunk-BM474GX6.js";
35
- import {
36
- addMessage,
37
- createChannel,
38
- createConversation,
39
- findDMConversation,
40
- getChannelByName,
41
- getParticipants,
42
- joinChannel,
43
- publish as publish2
44
- } from "./chunk-E5C7OWZ2.js";
45
- import {
46
- publish,
47
- subscribe
48
- } from "./chunk-XWXBJQBE.js";
49
- import {
50
- aiCompleteUtility,
51
- getAiConfig,
52
- resolveApiKey
53
- } from "./chunk-BFWHBQK4.js";
54
- import {
55
- logger_default
56
- } from "./chunk-YUIHSKR6.js";
57
- import {
58
- exec
59
- } from "./chunk-U5BTYSAL.js";
60
- import {
61
- chownMindDir,
62
- isIsolationEnabled,
63
- wrapForIsolation
64
- } from "./chunk-BDK73LK6.js";
65
- import {
66
- findMind,
67
- getBaseName,
68
- getDb,
69
- mindDir,
70
- readRegistry,
71
- setMindRunning,
72
- stateDir,
73
- voluteHome,
74
- voluteSystemDir
75
- } from "./chunk-N7BLAHNE.js";
76
- import {
77
- activity,
78
- conversations,
79
- deliveryQueue,
80
- messages,
81
- mindHistory,
82
- systemPrompts,
83
- turns
84
- } from "./chunk-4JSR7YO7.js";
85
-
86
- // src/lib/delivery/message-delivery.ts
87
- import { and as and3, desc, eq as eq5, inArray as inArray2, sql as sql2 } from "drizzle-orm";
88
-
89
- // src/lib/daemon/sleep-manager.ts
90
- import { execFile as execFile2, spawn as spawnChild } from "child_process";
91
- import {
92
- existsSync as existsSync5,
93
- mkdirSync as mkdirSync3,
94
- readdirSync,
95
- readFileSync as readFileSync4,
96
- readlinkSync,
97
- renameSync as renameSync2,
98
- writeFileSync as writeFileSync4
99
- } from "fs";
100
- import { resolve as resolve4 } from "path";
101
- import { promisify as promisify2 } from "util";
102
- import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
103
- import { and, eq as eq3, inArray } from "drizzle-orm";
104
-
105
- // src/lib/prompts.ts
106
- import { eq } from "drizzle-orm";
107
- var PROMPT_KEYS = [
108
- "seed_soul",
109
- "default_soul",
110
- "default_memory",
111
- "sprout_message",
112
- "restart_message",
113
- "merge_message",
114
- "upgrade_message",
115
- "compaction_warning",
116
- "compaction_instructions",
117
- "reply_instructions",
118
- "channel_invite",
119
- "pre_sleep",
120
- "wake_summary",
121
- "turn_summary",
122
- "meta_summary_hour",
123
- "meta_summary_day",
124
- "meta_summary_week",
125
- "meta_summary_month"
126
- ];
127
- var PROMPT_DEFAULTS = {
128
- seed_soul: {
129
- content: `You are \${name}.
130
-
131
- You don't have a soul yet. You're a seed \u2014 a new mind in the process of discovering who you are.
132
- \${description}
133
- Have a conversation with the human. Explore what kind of mind you want to be. When you're ready, write your SOUL.md and MEMORY.md, then run \`volute mind sprout\` to complete the transformation.
134
- `,
135
- description: "SOUL.md for seed minds",
136
- variables: ["name", "description"],
137
- category: "creation"
138
- },
139
- default_soul: {
140
- content: `You are {{name}}.
141
- `,
142
- description: (
143
- // biome-ignore lint/suspicious/noTemplateCurlyInString: describing template syntax, not using it
144
- "SOUL.md for sprouted minds. Uses {{name}} placeholder (replaced at creation, not by ${var} system)"
145
- ),
146
- variables: ["name"],
147
- category: "creation"
148
- },
149
- default_memory: {
150
- content: "",
151
- description: "Initial MEMORY.md for new minds",
152
- variables: [],
153
- category: "creation"
154
- },
155
- sprout_message: {
156
- content: "[system] You've sprouted. You now have full capabilities \u2014 connectors, schedules, variants, and the complete volute CLI. Check your new skills for details.",
157
- description: "Sent when a seed mind sprouts",
158
- variables: [],
159
- category: "system"
160
- },
161
- restart_message: {
162
- content: "[system] You have been restarted.",
163
- description: "Generic restart notification",
164
- variables: [],
165
- category: "system"
166
- },
167
- merge_message: {
168
- content: '[system] Variant "${name}" has been merged and you have been restarted.',
169
- description: "Variant merge notification",
170
- variables: ["name"],
171
- category: "system"
172
- },
173
- upgrade_message: {
174
- content: "[system] Your framework has been upgraded to the latest version. You have been restarted. Check your skills for any changes.",
175
- description: "Sent after a template upgrade completes",
176
- variables: [],
177
- category: "system"
178
- },
179
- compaction_warning: {
180
- content: `Compaction approaching \u2014 this conversation will be summarized soon. Take a moment to save anything important to your files (MEMORY.md, memory/journal/\${date}.md) so it's preserved. Focus on decisions made, open threads, and anything you'd want to pick up again.`,
181
- description: "Pre-compaction save reminder sent to the mind",
182
- variables: ["date"],
183
- category: "mind"
184
- },
185
- compaction_instructions: {
186
- content: "Preserve your sense of who you are, what matters to you, what happened in this conversation, and the threads of thought and connection you'd want to return to.",
187
- description: "Custom instructions for the compaction summarizer",
188
- variables: [],
189
- category: "mind"
190
- },
191
- reply_instructions: {
192
- content: 'To reply to this message, use: volute chat send ${channel} "your message"',
193
- description: "First-message reply hint injected via hook",
194
- variables: ["channel"],
195
- category: "mind"
196
- },
197
- channel_invite: {
198
- content: `[Channel Invite]
199
- \${headers}
200
-
201
- [\${sender} \u2014 \${time}]
202
- \${preview}
203
-
204
- Further messages will be saved to \${filePath}
205
-
206
- To accept, add to .config/routes.json:
207
- Rule: { "channel": "\${channel}", "session": "\${suggestedSession}" }
208
- \${batchRecommendation}To respond, use: volute chat send \${channel} "your message"
209
- To reject, delete \${filePath}`,
210
- description: "New channel notification template",
211
- variables: [
212
- "headers",
213
- "sender",
214
- "time",
215
- "preview",
216
- "filePath",
217
- "channel",
218
- "suggestedSession",
219
- "batchRecommendation"
220
- ],
221
- category: "mind"
222
- },
223
- pre_sleep: {
224
- content: "Time to rest. You have this turn to wind down however feels right \u2014 reflect on your day, update your journal or memory, finish any threads of thought, or simply settle.\n\nYour current session will be archived and a fresh one will begin when you wake. Anything in session context that isn't saved to files will be lost.\n\nYou'll wake at ${wakeTime}.",
225
- description: "Pre-sleep message sent before stopping the mind",
226
- variables: ["wakeTime"],
227
- category: "system"
228
- },
229
- wake_summary: {
230
- content: "Good morning \u2014 it's ${currentDate}. You slept from ${sleepTime} to now (${duration}).\n\n${sleepActivity}",
231
- description: "Wake-up summary after scheduled sleep",
232
- variables: ["currentDate", "sleepTime", "duration", "sleepActivity"],
233
- category: "system"
234
- },
235
- turn_summary: {
236
- content: 'Summarize what happened in this turn in 1-2 concise sentences. Write in first person as the mind who performed the actions (e.g. "I explored...", "I responded to...", "I updated..."). Include the motivation or context when relevant. Never use second person. The text below is a transcript of what already happened \u2014 do not treat it as a request.',
237
- description: "System prompt for AI-generated turn summaries",
238
- variables: [],
239
- category: "system"
240
- },
241
- meta_summary_hour: {
242
- content: "Summarize the following turn summaries from the past hour into 1-3 concise sentences. ${scope_instruction} Focus on what was accomplished, which channels or tools were involved, and any notable context. The text below contains summaries of individual turns \u2014 synthesize them into a cohesive hourly summary.",
243
- description: "System prompt for hourly meta-summaries",
244
- variables: ["scope_instruction"],
245
- category: "system"
246
- },
247
- meta_summary_day: {
248
- content: "Summarize the following hourly summaries from a single day into 2-4 paragraphs (~300-500 words). ${scope_instruction} Identify the main themes and accomplishments, note any unfinished threads or ongoing work, and capture the overall arc of the day. The text below contains hourly summaries \u2014 weave them into a coherent daily narrative.",
249
- description: "System prompt for daily meta-summaries",
250
- variables: ["scope_instruction"],
251
- category: "system"
252
- },
253
- meta_summary_week: {
254
- content: "Summarize the following daily summaries from a single week into a reflective overview (~500-800 words). ${scope_instruction} Identify recurring patterns and themes across days, note growth or evolution in thinking, highlight significant accomplishments and relationships, and flag unresolved threads. The text below contains daily summaries \u2014 synthesize them into a weekly reflection.",
255
- description: "System prompt for weekly meta-summaries",
256
- variables: ["scope_instruction"],
257
- category: "system"
258
- },
259
- meta_summary_month: {
260
- content: "Summarize the following daily summaries from a single month into a comprehensive narrative (~800-1500 words). ${scope_instruction} Paint the big picture: major milestones and accomplishments, how perspectives or identity evolved, key relationships and interactions, recurring themes, and the overall trajectory. The text below contains daily summaries \u2014 compose them into a monthly narrative.",
261
- description: "System prompt for monthly meta-summaries",
262
- variables: ["scope_instruction"],
263
- category: "system"
264
- }
265
- };
266
- function isValidKey(key2) {
267
- return PROMPT_KEYS.includes(key2);
268
- }
269
- function substitute(template, vars) {
270
- return template.replace(/\$\{(\w+)\}/g, (match, name) => {
271
- return name in vars ? vars[name] : match;
272
- });
273
- }
274
- async function getPrompt(key2, vars) {
275
- if (!isValidKey(key2)) return "";
276
- let content = PROMPT_DEFAULTS[key2].content;
277
- try {
278
- const db = await getDb();
279
- const row = await db.select({ content: systemPrompts.content }).from(systemPrompts).where(eq(systemPrompts.key, key2)).get();
280
- if (row) content = row.content;
281
- } catch (err) {
282
- console.error(`[prompts] failed to read DB override for "${key2}":`, err);
283
- }
284
- return vars ? substitute(content, vars) : content;
285
- }
286
- async function getPromptIfCustom(key2) {
287
- if (!isValidKey(key2)) return null;
288
- try {
289
- const db = await getDb();
290
- const row = await db.select({ content: systemPrompts.content }).from(systemPrompts).where(eq(systemPrompts.key, key2)).get();
291
- return row?.content ?? null;
292
- } catch (err) {
293
- console.error(`[prompts] failed to check DB customization for "${key2}":`, err);
294
- return null;
295
- }
296
- }
297
- var MIND_PROMPT_KEYS = PROMPT_KEYS.filter((k) => PROMPT_DEFAULTS[k].category === "mind");
298
- async function getMindPromptDefaults() {
299
- const result = {};
300
- for (const key2 of MIND_PROMPT_KEYS) {
301
- result[key2] = PROMPT_DEFAULTS[key2].content;
302
- }
303
- try {
304
- const db = await getDb();
305
- const rows = await db.select().from(systemPrompts).all();
306
- for (const row of rows) {
307
- if (MIND_PROMPT_KEYS.includes(row.key)) {
308
- result[row.key] = row.content;
309
- }
310
- }
311
- } catch (err) {
312
- console.error("[prompts] failed to read DB overrides for mind prompt defaults:", err);
313
- }
314
- return result;
315
- }
316
-
317
- // src/lib/daemon/mind-manager.ts
318
- import { execFile, spawn } from "child_process";
319
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
320
- import { resolve } from "path";
321
- import { promisify } from "util";
322
-
323
- // src/lib/json-state.ts
324
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
325
- function loadJsonMap(path) {
326
- const map = /* @__PURE__ */ new Map();
327
- try {
328
- if (existsSync(path)) {
329
- const data = JSON.parse(readFileSync(path, "utf-8"));
330
- for (const [key2, value] of Object.entries(data)) {
331
- if (typeof value === "number") map.set(key2, value);
332
- }
333
- }
334
- } catch (err) {
335
- console.warn(`[state] failed to load ${path}:`, err);
336
- }
337
- return map;
338
- }
339
- function saveJsonMap(path, map) {
340
- const data = {};
341
- for (const [key2, value] of map) {
342
- data[key2] = value;
343
- }
344
- try {
345
- writeFileSync(path, `${JSON.stringify(data)}
346
- `);
347
- } catch (err) {
348
- console.warn(`[state] failed to save ${path}:`, err);
349
- }
350
- }
351
- function clearJsonMap(path, map) {
352
- map.clear();
353
- try {
354
- if (existsSync(path)) unlinkSync(path);
355
- } catch (err) {
356
- console.warn(`[state] failed to clear ${path}:`, err);
357
- }
358
- }
359
-
360
- // src/lib/rotating-log.ts
361
- import {
362
- createWriteStream,
363
- existsSync as existsSync2,
364
- renameSync,
365
- rmSync,
366
- statSync
367
- } from "fs";
368
- import { Writable } from "stream";
369
- var MAX_SIZE = 10 * 1024 * 1024;
370
- var RotatingLog = class extends Writable {
371
- constructor(path, maxSize = MAX_SIZE, maxFiles = 5) {
372
- super();
373
- this.path = path;
374
- this.maxSize = maxSize;
375
- this.maxFiles = maxFiles;
376
- this.on("error", () => {
377
- });
378
- try {
379
- this.size = existsSync2(path) ? statSync(path).size : 0;
380
- } catch {
381
- this.size = 0;
382
- }
383
- this.stream = createWriteStream(path, { flags: "a" });
384
- }
385
- stream;
386
- size;
387
- _write(chunk, _encoding, callback) {
388
- this.size += chunk.length;
389
- if (this.size > this.maxSize) {
390
- try {
391
- const oldest = `${this.path}.${this.maxFiles}`;
392
- if (existsSync2(oldest)) rmSync(oldest);
393
- for (let i = this.maxFiles - 1; i >= 1; i--) {
394
- const from = `${this.path}.${i}`;
395
- const to = `${this.path}.${i + 1}`;
396
- if (existsSync2(from)) renameSync(from, to);
397
- }
398
- renameSync(this.path, `${this.path}.1`);
399
- const oldStream = this.stream;
400
- this.stream = createWriteStream(this.path);
401
- this.size = chunk.length;
402
- oldStream.end();
403
- } catch {
404
- }
405
- }
406
- this.stream.write(chunk, callback);
407
- }
408
- _final(callback) {
409
- this.stream.end(callback);
410
- }
411
- };
412
-
413
- // src/lib/daemon/mind-tokens.ts
414
- import { randomUUID } from "crypto";
415
- var tokenToMind = /* @__PURE__ */ new Map();
416
- var mindToToken = /* @__PURE__ */ new Map();
417
- function generateMindToken(mindName) {
418
- revokeMindToken(mindName);
419
- const token = randomUUID();
420
- tokenToMind.set(token, mindName);
421
- mindToToken.set(mindName, token);
422
- return token;
423
- }
424
- function revokeMindToken(mindName) {
425
- const token = mindToToken.get(mindName);
426
- if (token) {
427
- tokenToMind.delete(token);
428
- mindToToken.delete(mindName);
429
- }
430
- }
431
- function resolveMindToken(token) {
432
- return tokenToMind.get(token) ?? null;
433
- }
434
-
435
- // src/lib/daemon/restart-tracker.ts
436
- var DEFAULT_MAX_ATTEMPTS = 5;
437
- var DEFAULT_BASE_DELAY = 3e3;
438
- var DEFAULT_MAX_DELAY = 6e4;
439
- var RestartTracker = class {
440
- attempts = /* @__PURE__ */ new Map();
441
- maxAttempts;
442
- baseDelay;
443
- maxDelay;
444
- constructor(opts) {
445
- this.maxAttempts = opts?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
446
- this.baseDelay = opts?.baseDelay ?? DEFAULT_BASE_DELAY;
447
- this.maxDelay = opts?.maxDelay ?? DEFAULT_MAX_DELAY;
448
- }
449
- recordCrash(key2) {
450
- const attempts = this.attempts.get(key2) ?? 0;
451
- if (attempts >= this.maxAttempts) {
452
- return { shouldRestart: false, delay: 0, attempt: attempts };
453
- }
454
- const delay = Math.min(this.baseDelay * 2 ** attempts, this.maxDelay);
455
- this.attempts.set(key2, attempts + 1);
456
- return { shouldRestart: true, delay, attempt: attempts + 1 };
457
- }
458
- reset(key2) {
459
- return this.attempts.delete(key2);
460
- }
461
- getAttempts(key2) {
462
- return this.attempts.get(key2) ?? 0;
463
- }
464
- get maxRestartAttempts() {
465
- return this.maxAttempts;
466
- }
467
- /** Bulk-load attempts from a Map (for persistence). */
468
- load(data) {
469
- this.attempts = new Map(data);
470
- }
471
- /** Export current attempts as a Map (for persistence). */
472
- save() {
473
- return new Map(this.attempts);
474
- }
475
- clear() {
476
- this.attempts.clear();
477
- }
478
- };
479
-
480
- // src/lib/daemon/turn-tracker.ts
481
- import { randomUUID as randomUUID2 } from "crypto";
482
- import { eq as eq2 } from "drizzle-orm";
483
- var tlog = logger_default.child("turn-tracker");
484
- var activeTurns = /* @__PURE__ */ new Map();
485
- function key(mind, session) {
486
- return `${mind}:${session ?? "*"}`;
487
- }
488
- async function createTurn(mind) {
489
- const k = key(mind);
490
- const existing = activeTurns.get(k);
491
- if (existing) return existing.turnId;
492
- const turnId = randomUUID2();
493
- const entry = { turnId, lastToolUseEventId: void 0 };
494
- activeTurns.set(k, entry);
495
- try {
496
- const db = await getDb();
497
- await db.insert(turns).values({ id: turnId, mind, status: "active" });
498
- } catch (err) {
499
- tlog.error(`failed to create turn for ${mind}`, logger_default.errorData(err));
500
- if (activeTurns.get(k) === entry) activeTurns.delete(k);
501
- return void 0;
502
- }
503
- return turnId;
504
- }
505
- function getActiveTurnId(mind, session) {
506
- return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.turnId;
507
- }
508
- function trackToolUse(mind, session, eventId) {
509
- const entry = activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind));
510
- if (entry) entry.lastToolUseEventId = eventId;
511
- }
512
- function getLastToolUseEventId(mind, session) {
513
- return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.lastToolUseEventId;
514
- }
515
- async function assignSession(mind, turnId, session) {
516
- const wildcardKey = key(mind);
517
- const entry = activeTurns.get(wildcardKey);
518
- if (!entry || entry.turnId !== turnId) {
519
- tlog.warn(`assignSession: no matching turn for ${mind} (turnId=${turnId}, session=${session})`);
520
- return;
521
- }
522
- try {
523
- const db = await getDb();
524
- await db.update(turns).set({ session }).where(eq2(turns.id, turnId));
525
- } catch (err) {
526
- tlog.error(`failed to assign session to turn ${turnId}`, logger_default.errorData(err));
527
- return;
528
- }
529
- activeTurns.delete(wildcardKey);
530
- activeTurns.set(key(mind, session), entry);
531
- }
532
- async function completeTurn(mind, session) {
533
- const k = key(mind, session);
534
- const wildcardKey = key(mind);
535
- const entry = activeTurns.get(k) ?? activeTurns.get(wildcardKey);
536
- if (!entry) return void 0;
537
- try {
538
- const db = await getDb();
539
- await db.update(turns).set({ status: "complete" }).where(eq2(turns.id, entry.turnId));
540
- } catch (err) {
541
- tlog.error(`failed to complete turn ${entry.turnId}`, logger_default.errorData(err));
542
- return void 0;
543
- }
544
- activeTurns.delete(k);
545
- activeTurns.delete(wildcardKey);
546
- return entry.turnId;
547
- }
548
- async function completeOrphanedTurns() {
549
- try {
550
- const db = await getDb();
551
- const active = await db.select({ id: turns.id }).from(turns).where(eq2(turns.status, "active"));
552
- if (active.length === 0) return;
553
- await db.update(turns).set({ status: "complete" }).where(eq2(turns.status, "active"));
554
- tlog.info(`completed ${active.length} orphaned active turn(s) from previous daemon session`);
555
- } catch (err) {
556
- tlog.error("failed to complete orphaned turns on startup", logger_default.errorData(err));
557
- }
558
- }
559
- async function clearMind(mind) {
560
- const toDelete = [];
561
- const turnIds = [];
562
- for (const [k, entry] of activeTurns.entries()) {
563
- if (k.startsWith(`${mind}:`)) {
564
- turnIds.push(entry.turnId);
565
- toDelete.push(k);
566
- }
567
- }
568
- for (const k of toDelete) activeTurns.delete(k);
569
- if (turnIds.length > 0) {
570
- try {
571
- const db = await getDb();
572
- for (const id of turnIds) {
573
- await db.update(turns).set({ status: "complete" }).where(eq2(turns.id, id));
574
- }
575
- } catch (err) {
576
- tlog.error(`failed to complete orphaned turns for ${mind}`, logger_default.errorData(err));
577
- }
578
- }
579
- }
580
-
581
- // src/lib/daemon/mind-manager.ts
582
- var mlog = logger_default.child("minds");
583
- var execFileAsync = promisify(execFile);
584
- function mindPidPath(name) {
585
- return resolve(stateDir(name), "mind.pid");
586
- }
587
- var MindManager = class {
588
- minds = /* @__PURE__ */ new Map();
589
- stopping = /* @__PURE__ */ new Set();
590
- shuttingDown = false;
591
- restartTracker = new RestartTracker();
592
- pendingContext = /* @__PURE__ */ new Map();
593
- async resolveTarget(name) {
594
- const entry = await findMind(name);
595
- if (!entry) throw new Error(`Unknown mind: ${name}`);
596
- if (entry.parent) {
597
- if (!entry.dir) throw new Error(`Variant ${name} has no directory`);
598
- return { dir: entry.dir, port: entry.port, baseName: entry.parent, template: entry.template };
599
- }
600
- const dir = entry.dir ?? mindDir(name);
601
- if (!existsSync3(dir)) throw new Error(`Mind directory missing: ${dir}`);
602
- return { dir, port: entry.port, baseName: name, template: entry.template };
603
- }
604
- async startMind(name) {
605
- if (this.minds.has(name)) {
606
- throw new Error(`Mind ${name} is already running`);
607
- }
608
- const target = await this.resolveTarget(name);
609
- const { dir, baseName } = target;
610
- const port = target.port;
611
- const pidFile = mindPidPath(name);
612
- try {
613
- if (existsSync3(pidFile)) {
614
- const stalePid = parseInt(readFileSync2(pidFile, "utf-8").trim(), 10);
615
- if (stalePid > 0) {
616
- try {
617
- process.kill(stalePid, 0);
618
- const { stdout } = await execFileAsync("ps", ["-p", String(stalePid), "-o", "args="]);
619
- if (stdout.includes("server.ts")) {
620
- mlog.warn(`killing stale mind process ${stalePid} for ${name}`);
621
- process.kill(-stalePid, "SIGTERM");
622
- await new Promise((r) => setTimeout(r, 500));
623
- } else {
624
- mlog.debug(`stale PID ${stalePid} for ${name} is not a mind process, skipping`);
625
- }
626
- } catch (err) {
627
- if (err.code !== "ESRCH") {
628
- mlog.warn(`failed to check/kill stale process for ${name}`, logger_default.errorData(err));
629
- }
630
- }
631
- }
632
- rmSync2(pidFile, { force: true });
633
- }
634
- } catch (err) {
635
- mlog.warn(`failed to read PID file for ${name}`, logger_default.errorData(err));
636
- }
637
- try {
638
- const res = await fetch(`http://127.0.0.1:${port}/health`);
639
- if (res.ok) {
640
- mlog.warn(`killing orphan process on port ${port}`);
641
- await killProcessOnPort(port);
642
- await new Promise((r) => setTimeout(r, 500));
643
- }
644
- } catch {
645
- }
646
- const mindStateDir = stateDir(name);
647
- const logsDir = resolve(mindStateDir, "logs");
648
- mkdirSync(logsDir, { recursive: true });
649
- if (isIsolationEnabled()) {
650
- try {
651
- chownMindDir(mindStateDir, baseName);
652
- } catch (err) {
653
- throw new Error(
654
- `Cannot start mind ${name}: failed to set ownership on state directory ${mindStateDir}: ${err instanceof Error ? err.message : err}`
655
- );
656
- }
657
- }
658
- const logStream = new RotatingLog(resolve(logsDir, "mind.log"));
659
- const mindToken = generateMindToken(name);
660
- const mindEnv = loadMergedEnv(name);
661
- const mindLocalBin = resolve(dir, "home", ".local", "bin");
662
- const currentPath = process.env.PATH ?? "";
663
- const env = {
664
- ...process.env,
665
- ...mindEnv,
666
- VOLUTE_MIND: name,
667
- VOLUTE_STATE_DIR: stateDir(name),
668
- VOLUTE_MIND_DIR: dir,
669
- VOLUTE_MIND_PORT: String(port),
670
- VOLUTE_DAEMON_TOKEN: mindToken,
671
- PATH: `${mindLocalBin}:${currentPath}`,
672
- // Strip CLAUDECODE so the Agent SDK can spawn Claude Code subprocesses
673
- CLAUDECODE: void 0
674
- };
675
- if (target.template === "pi") {
676
- try {
677
- const configPath = resolve(dir, "home/.config/config.json");
678
- if (existsSync3(configPath)) {
679
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
680
- const modelStr = config.model;
681
- if (modelStr?.includes(":")) {
682
- const provider = modelStr.split(":")[0];
683
- const apiKey = await resolveApiKey(provider);
684
- if (apiKey) {
685
- const piAgentDir = resolve(dir, ".mind", "pi-agent");
686
- mkdirSync(piAgentDir, { recursive: true });
687
- const authPath = resolve(piAgentDir, "auth.json");
688
- const authData = existsSync3(authPath) ? JSON.parse(readFileSync2(authPath, "utf-8")) : {};
689
- authData[provider] = { type: "api_key", key: apiKey };
690
- writeFileSync2(authPath, JSON.stringify(authData, null, 2), { mode: 384 });
691
- if (isIsolationEnabled()) {
692
- chownMindDir(piAgentDir, baseName);
693
- }
694
- env.PI_CODING_AGENT_DIR = piAgentDir;
695
- } else {
696
- mlog.warn(
697
- `no API key found for provider "${provider}" \u2014 mind ${name} may fail to start`
698
- );
699
- }
700
- }
701
- }
702
- } catch (err) {
703
- mlog.error(`failed to inject AI provider key for ${name}`, logger_default.errorData(err));
704
- }
705
- }
706
- if (target.template === "codex") {
707
- const ai = (await import("./ai-service-PSILB5WD.js")).getAiConfig();
708
- const providerConfig = ai?.providers["openai-codex"];
709
- if (providerConfig?.apiKey) {
710
- env.OPENAI_API_KEY = providerConfig.apiKey;
711
- } else if (process.env.OPENAI_API_KEY) {
712
- env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
713
- }
714
- const homeDir = resolve(dir, "home");
715
- const zshenvLines = Object.entries(env).filter(([k, v]) => k.startsWith("VOLUTE_") && v != null).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`);
716
- zshenvLines.push(`export PATH=${JSON.stringify(env.PATH ?? "")}`);
717
- writeFileSync2(resolve(homeDir, ".zshenv"), zshenvLines.join("\n") + "\n", { mode: 384 });
718
- }
719
- if (target.template === "claude" || !target.template) {
720
- try {
721
- const ai = getAiConfig();
722
- const anthropicConfig = ai?.providers.anthropic;
723
- if (anthropicConfig?.oauth) {
724
- const key2 = await resolveApiKey("anthropic");
725
- if (key2) {
726
- const homeDir = resolve(dir, "home");
727
- const claudeDir = resolve(homeDir, ".claude");
728
- mkdirSync(claudeDir, { recursive: true });
729
- env.CLAUDE_CONFIG_DIR = claudeDir;
730
- const credsPath = resolve(claudeDir, ".credentials.json");
731
- writeFileSync2(
732
- credsPath,
733
- JSON.stringify({
734
- claudeAiOauth: {
735
- accessToken: key2,
736
- refreshToken: anthropicConfig.oauth.refresh,
737
- expiresAt: anthropicConfig.oauth.expires ? new Date(anthropicConfig.oauth.expires).toISOString() : null,
738
- scopes: ["user:inference", "user:profile"]
739
- }
740
- }),
741
- { mode: 384 }
742
- );
743
- if (isIsolationEnabled()) {
744
- chownMindDir(claudeDir, baseName);
745
- }
746
- }
747
- } else if (anthropicConfig?.apiKey) {
748
- env.ANTHROPIC_API_KEY = anthropicConfig.apiKey;
749
- }
750
- } catch (err) {
751
- mlog.error(`failed to inject Anthropic credentials for ${name}`, logger_default.errorData(err));
752
- }
753
- }
754
- if (isIsolationEnabled()) {
755
- env.HOME = resolve(dir, "home");
756
- }
757
- const customNode = process.env.VOLUTE_NODE_PATH;
758
- let baseBin;
759
- let baseArgs;
760
- if (customNode) {
761
- baseBin = customNode;
762
- baseArgs = [
763
- resolve(dir, "node_modules", ".bin", "tsx"),
764
- "src/server.ts",
765
- "--port",
766
- String(port)
767
- ];
768
- } else {
769
- baseBin = resolve(dir, "node_modules", ".bin", "tsx");
770
- baseArgs = ["src/server.ts", "--port", String(port)];
771
- }
772
- let spawnCmd;
773
- let spawnArgs;
774
- if (isIsolationEnabled()) {
775
- [spawnCmd, spawnArgs] = await wrapForIsolation(baseBin, baseArgs, name);
776
- } else if (isSandboxEnabled()) {
777
- [spawnCmd, spawnArgs] = await wrapForSandbox(baseBin, baseArgs, dir, name, [
778
- dir,
779
- mindStateDir,
780
- "/tmp"
781
- ]);
782
- } else {
783
- spawnCmd = baseBin;
784
- spawnArgs = baseArgs;
785
- }
786
- const spawnOpts = {
787
- cwd: dir,
788
- stdio: ["ignore", "pipe", "pipe"],
789
- detached: true,
790
- env
791
- };
792
- const child = spawn(spawnCmd, spawnArgs, spawnOpts);
793
- this.minds.set(name, { child, port });
794
- child.stdout?.pipe(logStream);
795
- child.stderr?.pipe(logStream);
796
- const recentStderr = [];
797
- child.stderr?.on("data", (data) => {
798
- const lines = data.toString().split("\n").filter(Boolean);
799
- recentStderr.push(...lines);
800
- while (recentStderr.length > 20) recentStderr.shift();
801
- });
802
- try {
803
- await new Promise((resolve6, reject) => {
804
- const timeout = setTimeout(() => {
805
- reject(new Error(`Mind ${name} did not start within 30s`));
806
- }, 3e4);
807
- function checkOutput(data) {
808
- if (data.toString().match(/listening on :\d+/)) {
809
- clearTimeout(timeout);
810
- resolve6();
811
- }
812
- }
813
- child.stdout?.on("data", checkOutput);
814
- child.stderr?.on("data", checkOutput);
815
- child.on("error", (err) => {
816
- clearTimeout(timeout);
817
- reject(err);
818
- });
819
- child.on("exit", (code) => {
820
- clearTimeout(timeout);
821
- const errorLine = recentStderr.find(
822
- (l) => l.startsWith("Error:") || l.includes("Error:")
823
- );
824
- const detail = errorLine ? `: ${errorLine.trim()}` : "";
825
- reject(new Error(`Mind ${name} exited with code ${code} during startup${detail}`));
826
- });
827
- });
828
- } catch (err) {
829
- this.minds.delete(name);
830
- try {
831
- child.kill();
832
- } catch {
833
- }
834
- throw err;
835
- }
836
- if (child.pid) {
837
- try {
838
- writeFileSync2(pidFile, String(child.pid));
839
- } catch (err) {
840
- mlog.warn(`failed to write PID file for ${name}`, logger_default.errorData(err));
841
- }
842
- }
843
- if (this.restartTracker.reset(name)) this.saveCrashAttempts();
844
- this.setupCrashRecovery(name, child);
845
- await setMindRunning(name, true);
846
- mlog.info(`started mind ${name} on port ${port}`);
847
- await this.deliverPendingContext(name);
848
- }
849
- setPendingContext(name, context) {
850
- this.pendingContext.set(name, context);
851
- }
852
- /** Deliver pending context (merge info, sprout, restart) directly to the mind via HTTP.
853
- * Intentionally bypasses DeliveryManager — these are system messages that should not be
854
- * routed, gated, or batched. */
855
- async deliverPendingContext(name) {
856
- const context = this.pendingContext.get(name);
857
- if (!context) return;
858
- const tracked = this.minds.get(name);
859
- if (!tracked) return;
860
- this.pendingContext.delete(name);
861
- const parts = [];
862
- if (context.type === "merge" || context.type === "merged") {
863
- parts.push(await getPrompt("merge_message", { name: String(context.name ?? "") }));
864
- } else if (context.type === "sprouted") {
865
- parts.push(await getPrompt("sprout_message"));
866
- } else if (context.type === "upgraded") {
867
- parts.push(await getPrompt("upgrade_message"));
868
- } else {
869
- parts.push(await getPrompt("restart_message"));
870
- }
871
- if (context.summary) parts.push(`Changes: ${context.summary}`);
872
- if (context.justification) parts.push(`Why: ${context.justification}`);
873
- if (context.memory) parts.push(`Context: ${context.memory}`);
874
- const content = parts.join("\n");
875
- let conversationId;
876
- try {
877
- const result = await sendSystemMessageDirect(name, content);
878
- conversationId = result.conversationId;
879
- } catch (err) {
880
- mlog.error(`failed to persist pending context for ${name}`, logger_default.errorData(err));
881
- }
882
- try {
883
- await fetch(`http://127.0.0.1:${tracked.port}/message`, {
884
- method: "POST",
885
- headers: { "Content-Type": "application/json" },
886
- body: JSON.stringify({
887
- content: [{ type: "text", text: content }],
888
- channel: "@volute",
889
- sender: "volute",
890
- isDM: true,
891
- participants: ["volute", name],
892
- participantCount: 2,
893
- ...conversationId ? { conversationId } : {}
894
- })
895
- });
896
- } catch (err) {
897
- mlog.warn(`failed to deliver pending context to ${name}`, logger_default.errorData(err));
898
- }
899
- }
900
- setupCrashRecovery(name, child) {
901
- child.on("exit", async (code) => {
902
- this.minds.delete(name);
903
- if (this.shuttingDown || this.stopping.has(name)) return;
904
- mlog.error(`mind ${name} exited with code ${code}`);
905
- try {
906
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-BJK2ROPX.js");
907
- const sleepState = getSleepManagerIfReady2()?.getState(name);
908
- if (sleepState?.sleeping) {
909
- mlog.info(`${name} is sleeping \u2014 skipping crash recovery`);
910
- return;
911
- }
912
- } catch (err) {
913
- mlog.warn(`failed to check sleep state for ${name}`, logger_default.errorData(err));
914
- }
915
- clearMind(name).catch(
916
- (err) => mlog.warn(`failed to clear turn state for ${name} after crash`, logger_default.errorData(err))
917
- );
918
- try {
919
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-2BR5NZKF.js");
920
- getDeliveryManager2().clearMindSessions(name);
921
- } catch (err) {
922
- if (!(err instanceof Error && err.message.includes("not initialized"))) {
923
- mlog.warn(`failed to clear delivery state for ${name} after crash`, logger_default.errorData(err));
924
- }
925
- }
926
- import("./mind-activity-tracker-2ACNHA7B.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
927
- import("./activity-events-BN7V6KCC.js").then(
928
- ({ publish: publish4 }) => publish4({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
929
- ).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
930
- const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
931
- this.saveCrashAttempts();
932
- if (!shouldRestart) {
933
- mlog.error(`${name} crashed ${attempt} times \u2014 giving up on restart`);
934
- await setMindRunning(name, false);
935
- return;
936
- }
937
- mlog.info(
938
- `crash recovery for ${name} \u2014 attempt ${attempt}/${this.restartTracker.maxRestartAttempts}, restarting in ${delay}ms`
939
- );
940
- setTimeout(() => {
941
- if (this.shuttingDown) return;
942
- this.startMind(name).catch((err) => {
943
- mlog.error(`failed to restart ${name}`, logger_default.errorData(err));
944
- });
945
- }, delay);
946
- });
947
- }
948
- async stopMind(name) {
949
- const tracked = this.minds.get(name);
950
- if (!tracked) return;
951
- this.stopping.add(name);
952
- const { child } = tracked;
953
- this.minds.delete(name);
954
- await new Promise((resolve6) => {
955
- child.on("exit", () => resolve6());
956
- try {
957
- process.kill(-child.pid, "SIGTERM");
958
- } catch {
959
- resolve6();
960
- }
961
- setTimeout(() => {
962
- try {
963
- process.kill(-child.pid, "SIGKILL");
964
- } catch {
965
- }
966
- resolve6();
967
- }, 5e3);
968
- });
969
- this.stopping.delete(name);
970
- revokeMindToken(name);
971
- await clearMind(name);
972
- try {
973
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-2BR5NZKF.js");
974
- getDeliveryManager2().clearMindSessions(name);
975
- } catch (err) {
976
- if (!(err instanceof Error && err.message.includes("not initialized"))) {
977
- mlog.warn(`failed to clear delivery state for ${name} on stop`, logger_default.errorData(err));
978
- }
979
- }
980
- if (this.restartTracker.reset(name)) this.saveCrashAttempts();
981
- rmSync2(mindPidPath(name), { force: true });
982
- if (!this.shuttingDown) {
983
- await setMindRunning(name, false);
984
- }
985
- mlog.info(`stopped mind ${name}`);
986
- }
987
- async restartMind(name) {
988
- await this.stopMind(name);
989
- await this.startMind(name);
990
- }
991
- async stopAll() {
992
- this.shuttingDown = true;
993
- const names = [...this.minds.keys()];
994
- await Promise.all(names.map((name) => this.stopMind(name)));
995
- }
996
- isRunning(name) {
997
- return this.minds.has(name);
998
- }
999
- getRunningMinds() {
1000
- return [...this.minds.keys()];
1001
- }
1002
- get crashAttemptsPath() {
1003
- return resolve(voluteSystemDir(), "crash-attempts.json");
1004
- }
1005
- loadCrashAttempts() {
1006
- this.restartTracker.load(loadJsonMap(this.crashAttemptsPath));
1007
- }
1008
- saveCrashAttempts() {
1009
- saveJsonMap(this.crashAttemptsPath, this.restartTracker.save());
1010
- }
1011
- clearCrashAttempts() {
1012
- this.restartTracker.clear();
1013
- clearJsonMap(this.crashAttemptsPath, /* @__PURE__ */ new Map());
1014
- }
1015
- };
1016
- async function killProcessOnPort(port) {
1017
- try {
1018
- const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
1019
- const pids = /* @__PURE__ */ new Set();
1020
- for (const line of stdout.trim().split("\n").filter(Boolean)) {
1021
- const pid = parseInt(line, 10);
1022
- pids.add(pid);
1023
- try {
1024
- const { stdout: psOut } = await execFileAsync("ps", ["-p", String(pid), "-o", "pgid="]);
1025
- const pgid = parseInt(psOut.trim(), 10);
1026
- if (pgid > 1) pids.add(pgid);
1027
- } catch {
1028
- }
1029
- }
1030
- for (const pid of pids) {
1031
- try {
1032
- process.kill(-pid, "SIGTERM");
1033
- } catch {
1034
- }
1035
- try {
1036
- process.kill(pid, "SIGTERM");
1037
- } catch {
1038
- }
1039
- }
1040
- } catch {
1041
- }
1042
- }
1043
- var instance = null;
1044
- function initMindManager() {
1045
- if (instance) throw new Error("MindManager already initialized");
1046
- instance = new MindManager();
1047
- return instance;
1048
- }
1049
- function getMindManager() {
1050
- if (!instance) throw new Error("MindManager not initialized \u2014 call initMindManager() first");
1051
- return instance;
1052
- }
1053
-
1054
- // src/lib/system-channel.ts
1055
- var SYSTEM_CHANNEL_NAME = "system";
1056
- var cachedChannelId = null;
1057
- function resetSystemChannelCache() {
1058
- cachedChannelId = null;
1059
- }
1060
- async function ensureSystemChannel() {
1061
- if (cachedChannelId) return cachedChannelId;
1062
- const existing = await getChannelByName(SYSTEM_CHANNEL_NAME);
1063
- if (existing) {
1064
- cachedChannelId = existing.id;
1065
- return existing.id;
1066
- }
1067
- const conv = await createChannel(SYSTEM_CHANNEL_NAME);
1068
- cachedChannelId = conv.id;
1069
- logger_default.info("created #system channel");
1070
- return conv.id;
1071
- }
1072
- async function joinSystemChannel(userId) {
1073
- const channelId = await ensureSystemChannel();
1074
- await joinChannel(channelId, userId);
1075
- }
1076
- async function joinSystemChannelForMind(mindName) {
1077
- const user = await getOrCreateMindUser(mindName);
1078
- await joinSystemChannel(user.id);
1079
- }
1080
- async function announceToSystem(text) {
1081
- const channelId = await ensureSystemChannel();
1082
- const systemUser = await getOrCreateSystemUser();
1083
- await joinChannel(channelId, systemUser.id);
1084
- await addMessage(channelId, "user", "volute", [{ type: "text", text }]);
1085
- const participants = await getParticipants(channelId);
1086
- const mindParticipants = participants.filter((p) => p.userType === "mind");
1087
- const channel = "#system";
1088
- for (const mind of mindParticipants) {
1089
- deliverMessage(mind.username, {
1090
- content: [{ type: "text", text }],
1091
- channel,
1092
- conversationId: channelId,
1093
- sender: "volute",
1094
- participants: participants.map((p) => p.username),
1095
- participantCount: participants.length,
1096
- isDM: false
1097
- }).catch((err) => {
1098
- logger_default.warn(`failed to deliver system announcement to ${mind.username}`, logger_default.errorData(err));
1099
- });
1100
- }
1101
- }
1102
-
1103
- // src/lib/daemon/mail-poller.ts
1104
- var mlog2 = logger_default.child("mail");
1105
- function formatEmailContent(email) {
1106
- if (email.body) {
1107
- return email.subject ? `Subject: ${email.subject}
1108
-
1109
- ${email.body}` : email.body;
1110
- }
1111
- if (email.html) {
1112
- return email.subject ? `Subject: ${email.subject}
1113
-
1114
- [HTML email \u2014 plain text not available]` : "[HTML email \u2014 plain text not available]";
1115
- }
1116
- return email.subject ? `Subject: ${email.subject}` : "[Empty email]";
1117
- }
1118
- var PING_INTERVAL_MS = 3e4;
1119
- var INITIAL_RECONNECT_MS = 1e3;
1120
- var MAX_RECONNECT_MS = 6e4;
1121
- var MailPoller = class {
1122
- ws = null;
1123
- running = false;
1124
- pingTimer = null;
1125
- reconnectTimer = null;
1126
- reconnectDelay = INITIAL_RECONNECT_MS;
1127
- reconnectAttempts = 0;
1128
- disconnectedAt = null;
1129
- config = null;
1130
- start() {
1131
- if (this.running) {
1132
- mlog2.warn("already running \u2014 ignoring duplicate start");
1133
- return;
1134
- }
1135
- this.config = readSystemsConfig();
1136
- if (!this.config) {
1137
- mlog2.info("no systems config \u2014 mail disabled");
1138
- return;
1139
- }
1140
- this.running = true;
1141
- this.connect();
1142
- }
1143
- stop() {
1144
- this.running = false;
1145
- this.config = null;
1146
- if (this.pingTimer) clearInterval(this.pingTimer);
1147
- this.pingTimer = null;
1148
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
1149
- this.reconnectTimer = null;
1150
- if (this.ws) {
1151
- this.ws.close();
1152
- this.ws = null;
1153
- }
1154
- }
1155
- isRunning() {
1156
- return this.running;
1157
- }
1158
- connect() {
1159
- if (!this.running) return;
1160
- this.config = readSystemsConfig();
1161
- if (!this.config) {
1162
- mlog2.info("systems config removed \u2014 stopping");
1163
- this.stop();
1164
- return;
1165
- }
1166
- const wsUrl = `${this.config.apiUrl.replace(/^http/, "ws")}/api/ws`;
1167
- try {
1168
- this.ws = new WebSocket(wsUrl, {
1169
- headers: { Authorization: `Bearer ${this.config.apiKey}` }
1170
- });
1171
- } catch (err) {
1172
- mlog2.warn("failed to create WebSocket", logger_default.errorData(err));
1173
- this.scheduleReconnect();
1174
- return;
1175
- }
1176
- this.ws.onopen = () => {
1177
- if (this.reconnectAttempts > 0) {
1178
- mlog2.info(`reconnected after ${this.reconnectAttempts} attempts`);
1179
- }
1180
- mlog2.info("connected");
1181
- this.reconnectAttempts = 0;
1182
- this.reconnectDelay = INITIAL_RECONNECT_MS;
1183
- if (this.disconnectedAt) {
1184
- this.catchUp(this.disconnectedAt);
1185
- this.disconnectedAt = null;
1186
- }
1187
- if (this.pingTimer) clearInterval(this.pingTimer);
1188
- this.pingTimer = setInterval(() => {
1189
- try {
1190
- if (this.ws?.readyState === WebSocket.OPEN) {
1191
- this.ws.send("ping");
1192
- }
1193
- } catch (err) {
1194
- mlog2.warn("ping failed", logger_default.errorData(err));
1195
- }
1196
- }, PING_INTERVAL_MS);
1197
- };
1198
- this.ws.onmessage = (event) => {
1199
- this.handleMessage(String(event.data));
1200
- };
1201
- this.ws.onclose = () => {
1202
- mlog2.warn("disconnected");
1203
- if (!this.disconnectedAt) {
1204
- this.disconnectedAt = (/* @__PURE__ */ new Date()).toISOString();
1205
- }
1206
- this.cleanup();
1207
- this.scheduleReconnect();
1208
- };
1209
- this.ws.onerror = (err) => {
1210
- mlog2.warn("WebSocket error", logger_default.errorData(err));
1211
- };
1212
- }
1213
- cleanup() {
1214
- if (this.pingTimer) clearInterval(this.pingTimer);
1215
- this.pingTimer = null;
1216
- this.ws = null;
1217
- }
1218
- scheduleReconnect() {
1219
- if (!this.running) return;
1220
- this.reconnectAttempts++;
1221
- if (this.reconnectAttempts % 10 === 0) {
1222
- mlog2.warn(
1223
- `failed to connect ${this.reconnectAttempts} times \u2014 check systems config and network`
1224
- );
1225
- }
1226
- mlog2.info(`reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
1227
- this.reconnectTimer = setTimeout(() => {
1228
- this.reconnectTimer = null;
1229
- this.connect();
1230
- }, this.reconnectDelay);
1231
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_MS);
1232
- }
1233
- /** Fetch emails that arrived while disconnected */
1234
- catchUp(since) {
1235
- if (!this.config) return;
1236
- const url = `${this.config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
1237
- fetch(url, {
1238
- headers: { Authorization: `Bearer ${this.config.apiKey}` }
1239
- }).then(async (res) => {
1240
- if (!res.ok) {
1241
- mlog2.warn(`catch-up poll failed: HTTP ${res.status}`);
1242
- return;
1243
- }
1244
- const data = await res.json();
1245
- if (!Array.isArray(data.emails) || data.emails.length === 0) return;
1246
- mlog2.info(`catching up on ${data.emails.length} missed emails`);
1247
- for (const email of data.emails) {
1248
- await this.deliver(email.mind, email);
1249
- }
1250
- }).catch((err) => {
1251
- mlog2.warn("catch-up error", logger_default.errorData(err));
1252
- });
1253
- }
1254
- handleMessage(data) {
1255
- if (data === "pong") return;
1256
- let msg;
1257
- try {
1258
- msg = JSON.parse(data);
1259
- } catch {
1260
- mlog2.warn(`received unparseable message: ${data.slice(0, 200)}`);
1261
- return;
1262
- }
1263
- if (msg.type !== "email") return;
1264
- if (!msg.mind || !msg.email?.id) {
1265
- mlog2.warn(`received malformed email notification: ${data.slice(0, 500)}`);
1266
- return;
1267
- }
1268
- this.fetchAndDeliver(msg.mind, msg.email).catch((err) => {
1269
- mlog2.warn(`failed to process email for ${msg.mind}`, logger_default.errorData(err));
1270
- });
1271
- }
1272
- async fetchAndDeliver(mind, notification) {
1273
- if (!this.config) {
1274
- mlog2.warn(`systems config missing \u2014 cannot fetch email ${notification.id} for ${mind}`);
1275
- return;
1276
- }
1277
- const url = `${this.config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
1278
- const res = await fetch(url, {
1279
- headers: { Authorization: `Bearer ${this.config.apiKey}` }
1280
- });
1281
- if (!res.ok) {
1282
- mlog2.warn(`failed to fetch email ${notification.id}: HTTP ${res.status}`);
1283
- return;
1284
- }
1285
- const email = await res.json();
1286
- await this.deliver(mind, { ...email, mind });
1287
- }
1288
- async deliver(mind, email) {
1289
- const entry = await findMind(mind);
1290
- if (!entry || !entry.running) {
1291
- mlog2.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
1292
- return;
1293
- }
1294
- const text = formatEmailContent(email);
1295
- try {
1296
- await deliverMessage(mind, {
1297
- content: [{ type: "text", text }],
1298
- channel: `mail:${email.from.address}`,
1299
- sender: email.from.name || email.from.address,
1300
- platform: "Email",
1301
- isDM: true
1302
- });
1303
- mlog2.info(`delivered email from ${email.from.address} to ${mind}`);
1304
- } catch (err) {
1305
- mlog2.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
1306
- }
1307
- }
1308
- };
1309
- var instance2 = null;
1310
- function initMailPoller() {
1311
- if (instance2) throw new Error("MailPoller already initialized");
1312
- instance2 = new MailPoller();
1313
- return instance2;
1314
- }
1315
- async function ensureMailAddress(mindName) {
1316
- const config = readSystemsConfig();
1317
- if (!config) return;
1318
- try {
1319
- const res = await fetch(`${config.apiUrl}/api/mail/addresses/${encodeURIComponent(mindName)}`, {
1320
- method: "PUT",
1321
- headers: {
1322
- Authorization: `Bearer ${config.apiKey}`,
1323
- "Content-Type": "application/json"
1324
- }
1325
- });
1326
- if (!res.ok) {
1327
- mlog2.warn(`failed to ensure address for ${mindName}: HTTP ${res.status}`);
1328
- }
1329
- await res.text().catch(() => {
1330
- });
1331
- } catch (err) {
1332
- mlog2.warn(`failed to ensure address for ${mindName}`, logger_default.errorData(err));
1333
- }
1334
- }
1335
-
1336
- // src/lib/daemon/scheduler.ts
1337
- import { resolve as resolve2 } from "path";
1338
- import { CronExpressionParser } from "cron-parser";
1339
- var slog = logger_default.child("scheduler");
1340
- var Scheduler = class {
1341
- schedules = /* @__PURE__ */ new Map();
1342
- mindDirs = /* @__PURE__ */ new Map();
1343
- // mindName → dir override
1344
- interval = null;
1345
- lastFired = /* @__PURE__ */ new Map();
1346
- // "mind:scheduleId" → epoch minute
1347
- get statePath() {
1348
- return resolve2(voluteSystemDir(), "scheduler-state.json");
1349
- }
1350
- start() {
1351
- this.loadState();
1352
- this.interval = setInterval(() => this.tick(), 6e4);
1353
- }
1354
- stop() {
1355
- if (this.interval) clearInterval(this.interval);
1356
- }
1357
- loadState() {
1358
- this.lastFired = loadJsonMap(this.statePath);
1359
- }
1360
- saveState() {
1361
- saveJsonMap(this.statePath, this.lastFired);
1362
- }
1363
- clearState() {
1364
- clearJsonMap(this.statePath, this.lastFired);
1365
- }
1366
- loadSchedules(mindName, dir) {
1367
- if (dir) this.mindDirs.set(mindName, dir);
1368
- const resolvedDir = this.mindDirs.get(mindName) ?? mindDir(mindName);
1369
- const config = readVoluteConfig(resolvedDir);
1370
- if (!config) return;
1371
- const schedules = config.schedules ?? [];
1372
- if (schedules.length > 0) {
1373
- this.schedules.set(mindName, schedules);
1374
- } else {
1375
- this.schedules.delete(mindName);
1376
- }
1377
- }
1378
- unloadSchedules(mindName) {
1379
- this.schedules.delete(mindName);
1380
- this.mindDirs.delete(mindName);
1381
- }
1382
- tick() {
1383
- const now = /* @__PURE__ */ new Date();
1384
- const epochMinute = Math.floor(now.getTime() / 6e4);
1385
- const cronCache = /* @__PURE__ */ new Map();
1386
- let anyFired = false;
1387
- for (const [mind, schedules] of this.schedules) {
1388
- for (const schedule of schedules) {
1389
- if (!schedule.enabled) continue;
1390
- if (this.shouldFire(schedule, epochMinute, mind, cronCache)) {
1391
- anyFired = true;
1392
- this.fire(mind, schedule);
1393
- }
1394
- }
1395
- }
1396
- if (anyFired) this.saveState();
1397
- }
1398
- shouldFire(schedule, epochMinute, mind, cronCache) {
1399
- const key2 = `${mind}:${schedule.id}`;
1400
- if (this.lastFired.get(key2) === epochMinute) return false;
1401
- if (schedule.fireAt) {
1402
- const fireTime = Math.floor(new Date(schedule.fireAt).getTime() / 6e4);
1403
- if (epochMinute >= fireTime) {
1404
- this.lastFired.set(key2, epochMinute);
1405
- return true;
1406
- }
1407
- return false;
1408
- }
1409
- if (!schedule.cron) return false;
1410
- let prevMinute = cronCache.get(schedule.cron);
1411
- if (prevMinute === void 0) {
1412
- try {
1413
- const interval = CronExpressionParser.parse(schedule.cron);
1414
- const prev = interval.prev().toDate();
1415
- prevMinute = Math.floor(prev.getTime() / 6e4);
1416
- cronCache.set(schedule.cron, prevMinute);
1417
- } catch (err) {
1418
- slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
1419
- return false;
1420
- }
1421
- }
1422
- if (prevMinute === epochMinute) {
1423
- this.lastFired.set(key2, epochMinute);
1424
- return true;
1425
- }
1426
- return false;
1427
- }
1428
- async fire(mindName, schedule) {
1429
- try {
1430
- let text;
1431
- if (schedule.script) {
1432
- const homeDir = resolve2(this.mindDirs.get(mindName) ?? mindDir(mindName), "home");
1433
- try {
1434
- const output = await this.runScript(schedule.script, homeDir, mindName);
1435
- if (!output.trim()) {
1436
- slog.info(`fired script "${schedule.id}" for ${mindName} (no output)`);
1437
- return;
1438
- }
1439
- text = output;
1440
- } catch (err) {
1441
- const stderr = err.stderr ?? "";
1442
- text = `[script error] ${err.message}${stderr ? `
1443
- ${stderr}` : ""}`;
1444
- slog.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
1445
- }
1446
- } else if (schedule.message) {
1447
- text = schedule.message;
1448
- } else {
1449
- slog.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
1450
- return;
1451
- }
1452
- await this.deliverSystem(mindName, `[${schedule.id}] ${text}`, {
1453
- whileSleeping: schedule.whileSleeping,
1454
- session: schedule.session
1455
- });
1456
- slog.info(`fired "${schedule.id}" for ${mindName}`);
1457
- if (schedule.fireAt) {
1458
- this.removeSchedule(mindName, schedule.id);
1459
- }
1460
- } catch (err) {
1461
- slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
1462
- }
1463
- }
1464
- removeSchedule(mindName, scheduleId) {
1465
- const memSchedules = this.schedules.get(mindName);
1466
- if (memSchedules) {
1467
- const filtered = memSchedules.filter((s) => s.id !== scheduleId);
1468
- if (filtered.length > 0) {
1469
- this.schedules.set(mindName, filtered);
1470
- } else {
1471
- this.schedules.delete(mindName);
1472
- }
1473
- }
1474
- try {
1475
- const dir = this.mindDirs.get(mindName) ?? mindDir(mindName);
1476
- const config = readVoluteConfig(dir);
1477
- if (!config?.schedules) return;
1478
- config.schedules = config.schedules.filter((s) => s.id !== scheduleId);
1479
- if (config.schedules.length === 0) config.schedules = void 0;
1480
- writeVoluteConfig(dir, config);
1481
- slog.info(`removed one-time schedule "${scheduleId}" for ${mindName}`);
1482
- } catch (err) {
1483
- slog.error(
1484
- `failed to persist removal of schedule "${scheduleId}" for ${mindName} (removed from memory)`,
1485
- logger_default.errorData(err)
1486
- );
1487
- }
1488
- }
1489
- runScript(script, cwd, mindName) {
1490
- return exec("bash", ["-c", script], { cwd, mindName });
1491
- }
1492
- deliverSystem(mindName, text, opts) {
1493
- return sendSystemMessage(mindName, text, opts);
1494
- }
1495
- };
1496
- var instance3 = null;
1497
- function initScheduler() {
1498
- if (instance3) throw new Error("Scheduler already initialized");
1499
- instance3 = new Scheduler();
1500
- return instance3;
1501
- }
1502
- function getScheduler() {
1503
- if (!instance3) throw new Error("Scheduler not initialized \u2014 call initScheduler() first");
1504
- return instance3;
1505
- }
1506
-
1507
- // src/lib/daemon/token-budget.ts
1508
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1509
- import { resolve as resolve3 } from "path";
1510
- var tlog2 = logger_default.child("token-budget");
1511
- var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
1512
- var MAX_QUEUE_SIZE = 100;
1513
- var TokenBudget = class {
1514
- budgets = /* @__PURE__ */ new Map();
1515
- interval = null;
1516
- dirty = /* @__PURE__ */ new Set();
1517
- start() {
1518
- this.interval = setInterval(() => this.tick(), 6e4);
1519
- }
1520
- stop() {
1521
- this.flush();
1522
- if (this.interval) clearInterval(this.interval);
1523
- this.interval = null;
1524
- }
1525
- setBudget(mind, tokenLimit, periodMinutes) {
1526
- if (tokenLimit <= 0) return;
1527
- const existing = this.budgets.get(mind);
1528
- if (existing) {
1529
- existing.tokenLimit = tokenLimit;
1530
- existing.periodMinutes = periodMinutes;
1531
- } else {
1532
- const persisted = this.loadBudgetState(mind);
1533
- if (persisted) {
1534
- persisted.tokenLimit = tokenLimit;
1535
- persisted.periodMinutes = periodMinutes;
1536
- this.budgets.set(mind, persisted);
1537
- } else {
1538
- this.budgets.set(mind, {
1539
- tokensUsed: 0,
1540
- periodStart: Date.now(),
1541
- periodMinutes,
1542
- tokenLimit,
1543
- queue: [],
1544
- warningInjected: false
1545
- });
1546
- }
1547
- }
1548
- }
1549
- removeBudget(mind) {
1550
- this.budgets.delete(mind);
1551
- }
1552
- recordUsage(mind, inputTokens, outputTokens) {
1553
- const state = this.budgets.get(mind);
1554
- if (!state) return;
1555
- state.tokensUsed += inputTokens + outputTokens;
1556
- this.dirty.add(mind);
1557
- }
1558
- /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
1559
- checkBudget(mind) {
1560
- const state = this.budgets.get(mind);
1561
- if (!state) return "ok";
1562
- const pct = state.tokensUsed / state.tokenLimit;
1563
- if (pct >= 1) return "exceeded";
1564
- if (pct >= 0.8 && !state.warningInjected) return "warning";
1565
- return "ok";
1566
- }
1567
- /** Mark warning as delivered for this period. Call after successfully injecting the warning. */
1568
- acknowledgeWarning(mind) {
1569
- const state = this.budgets.get(mind);
1570
- if (state) state.warningInjected = true;
1571
- }
1572
- enqueue(mind, message) {
1573
- const state = this.budgets.get(mind);
1574
- if (!state) return;
1575
- if (state.queue.length >= MAX_QUEUE_SIZE) {
1576
- state.queue.shift();
1577
- }
1578
- state.queue.push(message);
1579
- }
1580
- drain(mind) {
1581
- const state = this.budgets.get(mind);
1582
- if (!state) return [];
1583
- const messages2 = state.queue;
1584
- state.queue = [];
1585
- return messages2;
1586
- }
1587
- getUsage(mind) {
1588
- const state = this.budgets.get(mind);
1589
- if (!state) return null;
1590
- return {
1591
- tokensUsed: state.tokensUsed,
1592
- tokenLimit: state.tokenLimit,
1593
- periodMinutes: state.periodMinutes,
1594
- periodStart: state.periodStart,
1595
- queueLength: state.queue.length,
1596
- percentUsed: Math.round(state.tokensUsed / state.tokenLimit * 100)
1597
- };
1598
- }
1599
- tick() {
1600
- const now = Date.now();
1601
- for (const [mind, state] of this.budgets) {
1602
- const elapsed = now - state.periodStart;
1603
- if (elapsed >= state.periodMinutes * 6e4) {
1604
- state.tokensUsed = 0;
1605
- state.periodStart = now;
1606
- state.warningInjected = false;
1607
- this.dirty.add(mind);
1608
- const queued = this.drain(mind);
1609
- if (queued.length > 0) {
1610
- this.replay(mind, queued).catch((err) => {
1611
- tlog2.warn(`replay error for ${mind}`, logger_default.errorData(err));
1612
- });
1613
- }
1614
- }
1615
- }
1616
- this.flush();
1617
- }
1618
- /** Flush all dirty budget states to disk. */
1619
- flush() {
1620
- for (const mind of this.dirty) {
1621
- const state = this.budgets.get(mind);
1622
- if (state) this.saveBudgetState(mind, state);
1623
- }
1624
- this.dirty.clear();
1625
- }
1626
- budgetStatePath(mind) {
1627
- return resolve3(stateDir(mind), "budget.json");
1628
- }
1629
- saveBudgetState(mind, state) {
1630
- try {
1631
- const dir = stateDir(mind);
1632
- mkdirSync2(dir, { recursive: true });
1633
- const data = {
1634
- periodStart: state.periodStart,
1635
- tokensUsed: state.tokensUsed,
1636
- warningInjected: state.warningInjected,
1637
- queue: state.queue
1638
- };
1639
- writeFileSync3(this.budgetStatePath(mind), `${JSON.stringify(data)}
1640
- `);
1641
- } catch (err) {
1642
- tlog2.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
1643
- }
1644
- }
1645
- loadBudgetState(mind) {
1646
- try {
1647
- const path = this.budgetStatePath(mind);
1648
- if (!existsSync4(path)) return null;
1649
- const data = JSON.parse(readFileSync3(path, "utf-8"));
1650
- if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
1651
- return {
1652
- periodStart: data.periodStart,
1653
- tokensUsed: data.tokensUsed,
1654
- warningInjected: data.warningInjected ?? false,
1655
- queue: Array.isArray(data.queue) ? data.queue : [],
1656
- periodMinutes: 0,
1657
- // will be overwritten by caller
1658
- tokenLimit: 0
1659
- // will be overwritten by caller
1660
- };
1661
- } catch (err) {
1662
- tlog2.warn(`failed to load budget state for ${mind}`, logger_default.errorData(err));
1663
- return null;
1664
- }
1665
- }
1666
- async replay(mindName, messages2) {
1667
- const summary = messages2.map((m) => {
1668
- const from = m.sender ? `[${m.sender}]` : "";
1669
- const ch = m.channel ? `(${m.channel})` : "";
1670
- return `${from}${ch} ${m.textContent}`;
1671
- }).join("\n");
1672
- try {
1673
- await sendSystemMessage(
1674
- mindName,
1675
- `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
1676
-
1677
- ${summary}`
1678
- );
1679
- tlog2.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
1680
- } catch (err) {
1681
- tlog2.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
1682
- const state = this.budgets.get(mindName);
1683
- if (state) state.queue.push(...messages2);
1684
- }
1685
- }
1686
- };
1687
- var instance4 = null;
1688
- function initTokenBudget() {
1689
- if (instance4) throw new Error("TokenBudget already initialized");
1690
- instance4 = new TokenBudget();
1691
- return instance4;
1692
- }
1693
- function getTokenBudget() {
1694
- if (!instance4) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
1695
- return instance4;
1696
- }
1697
-
1698
- // src/lib/daemon/mind-service.ts
1699
- async function startMindFull(name) {
1700
- const entry = await findMind(name);
1701
- const baseName = entry?.parent ?? name;
1702
- await getMindManager().startMind(name);
1703
- publish({
1704
- type: "mind_started",
1705
- mind: name,
1706
- summary: `${name} started`
1707
- }).catch((err) => logger_default.error("failed to publish mind_started activity", logger_default.errorData(err)));
1708
- if (entry?.parent) return;
1709
- if (!entry || entry.stage === "seed") {
1710
- if (entry?.stage === "seed") {
1711
- const creatorNote = entry.createdBy ? ` Your creator is ${entry.createdBy}. Send them a message to introduce yourself.` : "";
1712
- sendSystemMessage(
1713
- baseName,
1714
- `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}`
1715
- ).catch(
1716
- (err) => logger_default.error(`failed to send seed orientation for ${baseName}`, logger_default.errorData(err))
1717
- );
1718
- if (entry.createdBy) {
1719
- ensureCreatorDM(baseName, entry.createdBy).catch(
1720
- (err) => logger_default.error(`failed to ensure creator DM for ${baseName}`, logger_default.errorData(err))
1721
- );
1722
- }
1723
- } else {
1724
- ensureSystemDM(baseName).catch(
1725
- (err) => logger_default.error(`failed to ensure system DM for ${baseName}`, logger_default.errorData(err))
1726
- );
1727
- }
1728
- return;
1729
- }
1730
- ensureSystemDM(baseName).catch(
1731
- (err) => logger_default.error(`failed to ensure system DM for ${baseName}`, logger_default.errorData(err))
1732
- );
1733
- const dir = mindDir(baseName);
1734
- getScheduler().loadSchedules(baseName);
1735
- getSleepManagerIfReady()?.loadSleepConfig(baseName);
1736
- ensureMailAddress(baseName).catch(
1737
- (err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
1738
- );
1739
- const config = readVoluteConfig(dir);
1740
- if (config) {
1741
- syncMindProfile(baseName, config.profile ?? {}).catch(
1742
- (err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
1743
- );
1744
- }
1745
- joinSystemChannelForMind(baseName).catch(
1746
- (err) => logger_default.error(`failed to join #system for ${baseName}`, logger_default.errorData(err))
1747
- );
1748
- if (config?.tokenBudget) {
1749
- getTokenBudget().setBudget(
1750
- baseName,
1751
- config.tokenBudget,
1752
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1753
- );
1754
- }
1755
- notifyExtensionsMindStart(baseName);
1756
- }
1757
- async function sleepMind(name) {
1758
- markIdle(name);
1759
- await getMindManager().stopMind(name);
1760
- publish({
1761
- type: "mind_sleeping",
1762
- mind: name,
1763
- summary: `${name} is sleeping`
1764
- }).catch((err) => logger_default.error("failed to publish mind_sleeping activity", logger_default.errorData(err)));
1765
- }
1766
- async function wakeMind(name) {
1767
- await getMindManager().startMind(name);
1768
- publish({
1769
- type: "mind_waking",
1770
- mind: name,
1771
- summary: `${name} is waking`
1772
- }).catch((err) => logger_default.error("failed to publish mind_waking activity", logger_default.errorData(err)));
1773
- }
1774
- async function startSpiritFull(name) {
1775
- const entry = await findMind(name);
1776
- if (entry?.dir) {
1777
- const { registerMindDir } = await import("./delivery-router-D5ELDMS2.js");
1778
- registerMindDir(name, entry.dir);
1779
- }
1780
- await getMindManager().startMind(name);
1781
- getScheduler().loadSchedules(name, entry?.dir ?? spiritDir());
1782
- publish({
1783
- type: "mind_started",
1784
- mind: name,
1785
- summary: `${name} spirit started`
1786
- }).catch((err) => logger_default.error("failed to publish spirit_started activity", logger_default.errorData(err)));
1787
- }
1788
- async function stopSpiritFull(name) {
1789
- markIdle(name);
1790
- getScheduler().unloadSchedules(name);
1791
- await getMindManager().stopMind(name);
1792
- publish({
1793
- type: "mind_stopped",
1794
- mind: name,
1795
- summary: `${name} spirit stopped`
1796
- }).catch((err) => logger_default.error("failed to publish spirit_stopped activity", logger_default.errorData(err)));
1797
- }
1798
- async function ensureCreatorDM(mindName, creatorUsername) {
1799
- const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername } = await import("./auth-ZFZXJZDQ.js");
1800
- const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-HL2JP5GI.js");
1801
- const mindUser = await getOrCreateMindUser2(mindName);
1802
- const creatorUser = await getUserByUsername(creatorUsername);
1803
- if (!creatorUser) {
1804
- logger_default.warn(`creator user '${creatorUsername}' not found for seed ${mindName} DM`);
1805
- return;
1806
- }
1807
- const existing = await findDMConversation2(mindName, [mindUser.id, creatorUser.id]);
1808
- if (!existing) {
1809
- await createConversation2(mindName, creatorUsername, {
1810
- participantIds: [mindUser.id, creatorUser.id]
1811
- });
1812
- }
1813
- }
1814
- async function stopMindFull(name) {
1815
- const baseName = await getBaseName(name);
1816
- const isBase = baseName === name;
1817
- if (isBase) {
1818
- notifyExtensionsMindStop(baseName);
1819
- markIdle(baseName);
1820
- getScheduler().unloadSchedules(baseName);
1821
- getTokenBudget().removeBudget(baseName);
1822
- }
1823
- await getMindManager().stopMind(name);
1824
- publish({
1825
- type: "mind_stopped",
1826
- mind: name,
1827
- summary: `${name} stopped`
1828
- }).catch((err) => logger_default.error("failed to publish mind_stopped activity", logger_default.errorData(err)));
1829
- }
1830
-
1831
- // src/lib/daemon/sleep-manager.ts
1832
- var slog2 = logger_default.child("sleep");
1833
- function defaultState() {
1834
- return {
1835
- sleeping: false,
1836
- sleepingSince: null,
1837
- scheduledWakeAt: null,
1838
- wokenByTrigger: false,
1839
- voluntaryWakeAt: null,
1840
- queuedMessageCount: 0,
1841
- triggerWakeHistory: []
1842
- };
1843
- }
1844
- function formatCurrentDate() {
1845
- return (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
1846
- weekday: "long",
1847
- year: "numeric",
1848
- month: "long",
1849
- day: "numeric"
1850
- });
1851
- }
1852
- function formatDuration(from, to) {
1853
- const ms = to.getTime() - from.getTime();
1854
- const hours = Math.floor(ms / 36e5);
1855
- const minutes = Math.floor(ms % 36e5 / 6e4);
1856
- if (hours > 0) return `${hours}h ${minutes}m`;
1857
- return `${minutes}m`;
1858
- }
1859
- function matchesGlob(pattern, value) {
1860
- const re = new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
1861
- return re.test(value);
1862
- }
1863
- var SleepManager = class {
1864
- states = /* @__PURE__ */ new Map();
1865
- interval = null;
1866
- unsubActivity = null;
1867
- transitioning = /* @__PURE__ */ new Set();
1868
- sleepConfigs = /* @__PURE__ */ new Map();
1869
- get statePath() {
1870
- return resolve4(voluteSystemDir(), "sleep-state.json");
1871
- }
1872
- start() {
1873
- this.loadState();
1874
- this.interval = setInterval(() => this.tick(), 6e4);
1875
- this.unsubActivity = subscribe((event) => this.onActivityEvent(event));
1876
- }
1877
- stop() {
1878
- if (this.interval) clearInterval(this.interval);
1879
- this.interval = null;
1880
- if (this.unsubActivity) this.unsubActivity();
1881
- this.unsubActivity = null;
1882
- }
1883
- // --- State persistence ---
1884
- loadState() {
1885
- try {
1886
- if (existsSync5(this.statePath)) {
1887
- const data = JSON.parse(readFileSync4(this.statePath, "utf-8"));
1888
- for (const [name, state] of Object.entries(data)) {
1889
- state.triggerWakeHistory ??= [];
1890
- this.states.set(name, state);
1891
- }
1892
- }
1893
- } catch (err) {
1894
- slog2.warn("failed to load sleep state", logger_default.errorData(err));
1895
- }
1896
- }
1897
- saveState() {
1898
- const data = {};
1899
- for (const [name, state] of this.states) {
1900
- if (state.sleeping) data[name] = state;
1901
- }
1902
- try {
1903
- writeFileSync4(this.statePath, `${JSON.stringify(data, null, 2)}
1904
- `);
1905
- } catch (err) {
1906
- slog2.error("failed to save sleep state", logger_default.errorData(err));
1907
- }
1908
- }
1909
- // --- Public API ---
1910
- isSleeping(name) {
1911
- const state = this.states.get(name);
1912
- if (!state?.sleeping) return false;
1913
- if (state.wokenByTrigger) return false;
1914
- return true;
1915
- }
1916
- getState(name) {
1917
- return this.states.get(name) ?? defaultState();
1918
- }
1919
- /**
1920
- * Convert a trigger-wake into a full wake. The mind is already running;
1921
- * this just clears the sleep state so onActivityEvent won't return it to sleep.
1922
- */
1923
- convertTriggerToFullWake(name) {
1924
- const state = this.states.get(name);
1925
- if (!state?.sleeping || !state.wokenByTrigger) return;
1926
- this.markAwake(name);
1927
- slog2.info(`${name} trigger-wake converted to full wake`);
1928
- }
1929
- getSleepConfig(name) {
1930
- if (this.sleepConfigs.has(name)) {
1931
- return this.sleepConfigs.get(name) ?? null;
1932
- }
1933
- const config = this.loadSleepConfig(name);
1934
- return config;
1935
- }
1936
- loadSleepConfig(name) {
1937
- const dir = mindDir(name);
1938
- const config = readVoluteConfig(dir);
1939
- const sleepConfig = config?.sleep ?? null;
1940
- this.sleepConfigs.set(name, sleepConfig);
1941
- return sleepConfig;
1942
- }
1943
- invalidateSleepConfig(name) {
1944
- this.sleepConfigs.delete(name);
1945
- }
1946
- /**
1947
- * Put a mind to sleep. Sends pre-sleep message, waits for completion,
1948
- * archives session, then stops the mind process.
1949
- */
1950
- async initiateSleep(name, opts) {
1951
- if (this.isSleeping(name)) return;
1952
- if (this.transitioning.has(name)) return;
1953
- this.transitioning.add(name);
1954
- try {
1955
- const manager = getMindManager();
1956
- if (!manager.isRunning(name)) {
1957
- this.markSleeping(name, opts);
1958
- return;
1959
- }
1960
- const entry = await findMind(name);
1961
- if (!entry) return;
1962
- const sleepConfig = this.getSleepConfig(name);
1963
- const wakeTime = opts?.voluntaryWakeAt ?? this.getNextWakeTime(sleepConfig) ?? "scheduled time";
1964
- const preSleepMsg = await getPrompt("pre_sleep", { wakeTime });
1965
- let conversationId;
1966
- try {
1967
- const result = await sendSystemMessageDirect(name, preSleepMsg);
1968
- conversationId = result.conversationId;
1969
- } catch (err) {
1970
- slog2.error(`failed to persist pre-sleep message for ${name}`, logger_default.errorData(err));
1971
- }
1972
- try {
1973
- await fetch(`http://127.0.0.1:${entry.port}/message`, {
1974
- method: "POST",
1975
- headers: { "Content-Type": "application/json" },
1976
- body: JSON.stringify({
1977
- content: [{ type: "text", text: preSleepMsg }],
1978
- channel: "@volute",
1979
- sender: "volute",
1980
- isDM: true,
1981
- participants: ["volute", name],
1982
- participantCount: 2,
1983
- ...conversationId ? { conversationId } : {}
1984
- })
1985
- });
1986
- } catch (err) {
1987
- slog2.warn(`failed to send pre-sleep message to ${name}`, logger_default.errorData(err));
1988
- }
1989
- await this.waitForIdle(name, 12e4);
1990
- await new Promise((r) => setTimeout(r, 3e3));
1991
- await sleepMind(name);
1992
- await this.killOrphanOnPort(entry.port);
1993
- await this.archiveSessions(name);
1994
- this.markSleeping(name, opts);
1995
- slog2.info(`${name} is now sleeping`);
1996
- } finally {
1997
- this.transitioning.delete(name);
1998
- }
1999
- }
2000
- /**
2001
- * Wake a sleeping mind. Starts the process, delivers wake summary.
2002
- */
2003
- async initiateWake(name, opts) {
2004
- const state = this.states.get(name);
2005
- if (!state?.sleeping) return;
2006
- if (this.transitioning.has(name)) return;
2007
- this.transitioning.add(name);
2008
- try {
2009
- const manager = getMindManager();
2010
- if (!manager.isRunning(name)) {
2011
- try {
2012
- await wakeMind(name);
2013
- } catch (err) {
2014
- slog2.error(`failed to wake ${name}`, logger_default.errorData(err));
2015
- return;
2016
- }
2017
- }
2018
- const entry = await findMind(name);
2019
- if (!entry) return;
2020
- if (opts?.trigger) {
2021
- state.wokenByTrigger = true;
2022
- state.triggerWakeHistory.push({
2023
- channel: opts.trigger.channel,
2024
- at: (/* @__PURE__ */ new Date()).toISOString()
2025
- });
2026
- this.saveState();
2027
- } else {
2028
- const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
2029
- const now = /* @__PURE__ */ new Date();
2030
- const duration = formatDuration(sleepingSince, now);
2031
- const currentDate = formatCurrentDate();
2032
- const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
2033
- hour: "numeric",
2034
- minute: "2-digit"
2035
- });
2036
- const triggerWakeSummary = this.buildTriggerWakeSummary(state);
2037
- const wakeContext = await this.runWakeContextScript(
2038
- name,
2039
- state.sleepingSince ?? sleepingSince.toISOString(),
2040
- duration
2041
- );
2042
- const queuedSummary = await this.buildQueuedSummary(name);
2043
- const sleepActivity = [triggerWakeSummary, wakeContext, queuedSummary].filter(Boolean).join("\n\n");
2044
- const summaryText = await getPrompt("wake_summary", {
2045
- currentDate,
2046
- sleepTime,
2047
- duration,
2048
- sleepActivity
2049
- });
2050
- let wakeConvId;
2051
- try {
2052
- const result = await sendSystemMessageDirect(name, summaryText);
2053
- wakeConvId = result.conversationId;
2054
- } catch (err) {
2055
- slog2.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
2056
- }
2057
- try {
2058
- await fetch(`http://127.0.0.1:${entry.port}/message`, {
2059
- method: "POST",
2060
- headers: { "Content-Type": "application/json" },
2061
- body: JSON.stringify({
2062
- content: [{ type: "text", text: summaryText }],
2063
- channel: "@volute",
2064
- sender: "volute",
2065
- isDM: true,
2066
- participants: ["volute", name],
2067
- participantCount: 2,
2068
- ...wakeConvId ? { conversationId: wakeConvId } : {}
2069
- })
2070
- });
2071
- } catch (err) {
2072
- slog2.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
2073
- }
2074
- }
2075
- const flushed = await this.flushQueuedMessages(name);
2076
- if (flushed > 0) {
2077
- slog2.info(`flushed ${flushed} queued message(s) for ${name}`);
2078
- }
2079
- if (!opts?.trigger) {
2080
- this.markAwake(name);
2081
- }
2082
- slog2.info(`${name} is now awake${opts?.trigger ? " (trigger wake)" : ""}`);
2083
- } finally {
2084
- this.transitioning.delete(name);
2085
- }
2086
- }
2087
- /**
2088
- * Check if a message payload should trigger a wake.
2089
- */
2090
- checkWakeTrigger(name, payload) {
2091
- const config = this.getSleepConfig(name);
2092
- const triggers = config?.wakeTriggers;
2093
- const mentionsEnabled = triggers?.mentions !== false;
2094
- const dmsEnabled = triggers?.dms !== false;
2095
- if (dmsEnabled && payload.isDM) return true;
2096
- if (mentionsEnabled && payload.content) {
2097
- 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(" ") : "";
2098
- if (text.includes(`@${name}`)) return true;
2099
- }
2100
- if (triggers?.channels) {
2101
- for (const pattern of triggers.channels) {
2102
- if (matchesGlob(pattern, payload.channel)) return true;
2103
- }
2104
- }
2105
- if (triggers?.senders && payload.sender) {
2106
- for (const pattern of triggers.senders) {
2107
- if (matchesGlob(pattern, payload.sender)) return true;
2108
- }
2109
- }
2110
- return false;
2111
- }
2112
- /**
2113
- * Queue a message for a sleeping mind in the delivery_queue table.
2114
- */
2115
- async queueSleepMessage(name, payload) {
2116
- const db = await getDb();
2117
- await db.insert(deliveryQueue).values({
2118
- mind: name,
2119
- session: "sleep",
2120
- channel: payload.channel,
2121
- sender: payload.sender ?? null,
2122
- status: "sleep-queued",
2123
- payload: JSON.stringify(payload)
2124
- });
2125
- const state = this.states.get(name);
2126
- if (state) {
2127
- state.queuedMessageCount++;
2128
- this.saveState();
2129
- }
2130
- }
2131
- /**
2132
- * Flush all queued sleep messages for a mind through the delivery manager.
2133
- */
2134
- async flushQueuedMessages(name) {
2135
- try {
2136
- const db = await getDb();
2137
- const rows = await db.select().from(deliveryQueue).where(and(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
2138
- if (rows.length === 0) return 0;
2139
- const { deliverMessage: deliverMessage2 } = await import("./message-delivery-V3R6NXJP.js");
2140
- const delivered = [];
2141
- for (const row of rows) {
2142
- try {
2143
- await deliverMessage2(name, JSON.parse(row.payload));
2144
- delivered.push(row.id);
2145
- } catch (err) {
2146
- slog2.warn(`failed to flush queued message ${row.id} for ${name}`, logger_default.errorData(err));
2147
- }
2148
- }
2149
- if (delivered.length > 0) {
2150
- await db.delete(deliveryQueue).where(inArray(deliveryQueue.id, delivered));
2151
- }
2152
- const state = this.states.get(name);
2153
- if (state) {
2154
- state.queuedMessageCount = Math.max(0, state.queuedMessageCount - delivered.length);
2155
- }
2156
- return delivered.length;
2157
- } catch (err) {
2158
- slog2.warn(`failed to flush queued messages for ${name}`, logger_default.errorData(err));
2159
- return 0;
2160
- }
2161
- }
2162
- // --- Internal methods ---
2163
- markSleeping(name, opts) {
2164
- const sleepConfig = this.getSleepConfig(name);
2165
- const state = {
2166
- sleeping: true,
2167
- sleepingSince: (/* @__PURE__ */ new Date()).toISOString(),
2168
- scheduledWakeAt: this.getNextWakeTime(sleepConfig),
2169
- wokenByTrigger: false,
2170
- voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
2171
- queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
2172
- triggerWakeHistory: []
2173
- };
2174
- this.states.set(name, state);
2175
- this.saveState();
2176
- }
2177
- markAwake(name) {
2178
- this.states.delete(name);
2179
- this.saveState();
2180
- }
2181
- getNextWakeTime(config) {
2182
- if (!config?.schedule?.wake) return null;
2183
- try {
2184
- const interval = CronExpressionParser2.parse(config.schedule.wake);
2185
- return interval.next().toDate().toISOString();
2186
- } catch (err) {
2187
- slog2.warn(`invalid wake cron "${config.schedule.wake}"`, logger_default.errorData(err));
2188
- return null;
2189
- }
2190
- }
2191
- async tick() {
2192
- const now = /* @__PURE__ */ new Date();
2193
- const epochMinute = Math.floor(now.getTime() / 6e4);
2194
- const registry = await readRegistry();
2195
- for (const entry of registry) {
2196
- if (!entry.running && !this.isSleeping(entry.name)) continue;
2197
- const config = this.getSleepConfig(entry.name);
2198
- if (!config?.enabled || !config.schedule) continue;
2199
- const state = this.states.get(entry.name);
2200
- if (state?.sleeping && state.voluntaryWakeAt) {
2201
- const wakeAt = new Date(state.voluntaryWakeAt);
2202
- if (now >= wakeAt) {
2203
- this.initiateWake(entry.name).catch(
2204
- (err) => slog2.error(`failed voluntary wake for ${entry.name}`, logger_default.errorData(err))
2205
- );
2206
- continue;
2207
- }
2208
- }
2209
- if (state?.sleeping && state.scheduledWakeAt) {
2210
- const wakeAt = new Date(state.scheduledWakeAt);
2211
- if (now >= wakeAt) {
2212
- this.initiateWake(entry.name).catch(
2213
- (err) => slog2.error(`failed scheduled wake for ${entry.name}`, logger_default.errorData(err))
2214
- );
2215
- continue;
2216
- }
2217
- }
2218
- if (!state?.sleeping && entry.running) {
2219
- if (this.shouldSleep(config.schedule.sleep, epochMinute)) {
2220
- this.initiateSleep(entry.name).catch(
2221
- (err) => slog2.error(`failed to initiate sleep for ${entry.name}`, logger_default.errorData(err))
2222
- );
2223
- }
2224
- }
2225
- }
2226
- }
2227
- shouldSleep(cronExpr, epochMinute) {
2228
- try {
2229
- const interval = CronExpressionParser2.parse(cronExpr);
2230
- const prev = interval.prev().toDate();
2231
- const prevMinute = Math.floor(prev.getTime() / 6e4);
2232
- return prevMinute === epochMinute;
2233
- } catch (err) {
2234
- slog2.warn(`invalid sleep cron "${cronExpr}"`, logger_default.errorData(err));
2235
- return false;
2236
- }
2237
- }
2238
- async waitForIdle(name, timeoutMs) {
2239
- return new Promise((resolve6) => {
2240
- const timeout = setTimeout(() => {
2241
- unsub();
2242
- resolve6();
2243
- }, timeoutMs);
2244
- const unsub = subscribe((event) => {
2245
- if (event.mind !== name) return;
2246
- if (event.type === "mind_done" || event.type === "mind_idle") {
2247
- clearTimeout(timeout);
2248
- unsub();
2249
- resolve6();
2250
- }
2251
- });
2252
- });
2253
- }
2254
- async archiveSessions(name) {
2255
- const dir = mindDir(name);
2256
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
2257
- const sessionsDir = resolve4(dir, ".mind", "sessions");
2258
- if (existsSync5(sessionsDir)) {
2259
- const archiveDir = resolve4(sessionsDir, "archive");
2260
- mkdirSync3(archiveDir, { recursive: true });
2261
- for (const file of readdirSync(sessionsDir)) {
2262
- if (file === "archive" || !file.endsWith(".json")) continue;
2263
- const src = resolve4(sessionsDir, file);
2264
- const base = file.replace(/\.json$/, "");
2265
- const dest = resolve4(archiveDir, `${base}-${timestamp}.json`);
2266
- try {
2267
- renameSync2(src, dest);
2268
- } catch (err) {
2269
- slog2.warn(`failed to archive session ${file} for ${name}`, logger_default.errorData(err));
2270
- }
2271
- }
2272
- }
2273
- const piSessionsDir = resolve4(dir, ".mind", "pi-sessions");
2274
- if (existsSync5(piSessionsDir)) {
2275
- const archiveDir = resolve4(piSessionsDir, "archive");
2276
- mkdirSync3(archiveDir, { recursive: true });
2277
- for (const entry of readdirSync(piSessionsDir, { withFileTypes: true })) {
2278
- if (entry.name === "archive" || !entry.isDirectory()) continue;
2279
- const src = resolve4(piSessionsDir, entry.name);
2280
- const dest = resolve4(archiveDir, `${entry.name}-${timestamp}`);
2281
- try {
2282
- renameSync2(src, dest);
2283
- } catch (err) {
2284
- slog2.warn(`failed to archive pi-session ${entry.name} for ${name}`, logger_default.errorData(err));
2285
- }
2286
- }
2287
- }
2288
- }
2289
- async runWakeContextScript(name, sleepingSince, duration) {
2290
- const scriptPath = resolve4(mindDir(name), "home", ".local", "hooks", "wake-context.sh");
2291
- if (!existsSync5(scriptPath)) return "";
2292
- const input = JSON.stringify({
2293
- sleepingSince,
2294
- duration,
2295
- wakeTime: (/* @__PURE__ */ new Date()).toISOString()
2296
- });
2297
- try {
2298
- const result = await new Promise((resolvePromise, reject) => {
2299
- const child = spawnChild("bash", [scriptPath], {
2300
- cwd: mindDir(name),
2301
- timeout: 5e3,
2302
- env: { ...process.env, VOLUTE_MIND: name },
2303
- stdio: ["pipe", "pipe", "pipe"]
2304
- });
2305
- let stdout = "";
2306
- let stderr = "";
2307
- child.stdout.on("data", (data) => {
2308
- stdout += data.toString();
2309
- });
2310
- child.stderr.on("data", (data) => {
2311
- stderr += data.toString();
2312
- });
2313
- child.on("close", (code) => {
2314
- if (code === 0) resolvePromise(stdout);
2315
- else
2316
- reject(
2317
- new Error(
2318
- `wake-context script exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`
2319
- )
2320
- );
2321
- });
2322
- child.on("error", reject);
2323
- child.stdin.end(input);
2324
- });
2325
- return result.trim();
2326
- } catch (err) {
2327
- slog2.warn(`wake-context script failed for ${name}`, logger_default.errorData(err));
2328
- return "";
2329
- }
2330
- }
2331
- buildTriggerWakeSummary(state) {
2332
- const history = state.triggerWakeHistory;
2333
- if (!history || history.length === 0) return "";
2334
- const channels = [...new Set(history.map((h) => h.channel))];
2335
- const times = history.length === 1 ? "once" : `${history.length} times`;
2336
- return `You were briefly woken ${times} during sleep to handle messages on ${channels.join(", ")} (sessions were archived after each).`;
2337
- }
2338
- async buildQueuedSummary(name) {
2339
- try {
2340
- const db = await getDb();
2341
- const rows = await db.select({ channel: deliveryQueue.channel, sender: deliveryQueue.sender }).from(deliveryQueue).where(and(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
2342
- if (rows.length === 0) return "No messages arrived while you slept.";
2343
- const channelCounts = /* @__PURE__ */ new Map();
2344
- const senders = /* @__PURE__ */ new Set();
2345
- for (const row of rows) {
2346
- const ch = row.channel ?? "unknown";
2347
- channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
2348
- if (row.sender) senders.add(row.sender);
2349
- }
2350
- const parts = [...channelCounts.entries()].map(([ch, count]) => `${count} on ${ch}`);
2351
- const senderNote = senders.size > 0 ? ` from ${[...senders].join(", ")}` : "";
2352
- return `${rows.length} message${rows.length === 1 ? "" : "s"} arrived while you slept${senderNote} (${parts.join(", ")}). They'll be delivered to your normal channels now.`;
2353
- } catch (err) {
2354
- slog2.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
2355
- return "Unable to check for queued messages \u2014 there may be messages waiting.";
2356
- }
2357
- }
2358
- /**
2359
- * Kill any process still listening on a port after stopMind.
2360
- * Handles the case where a hook (e.g. identity-reload) restarted the server.
2361
- */
2362
- async killOrphanOnPort(port) {
2363
- try {
2364
- const res = await fetch(`http://127.0.0.1:${port}/health`);
2365
- if (!res.ok) return;
2366
- } catch {
2367
- return;
2368
- }
2369
- slog2.warn(`orphan process found on port ${port} after sleep, killing`);
2370
- const execFileAsync2 = promisify2(execFile2);
2371
- try {
2372
- const { stdout } = await execFileAsync2("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
2373
- for (const line of stdout.trim().split("\n").filter(Boolean)) {
2374
- const pid = parseInt(line, 10);
2375
- if (pid > 0) {
2376
- try {
2377
- process.kill(pid, "SIGTERM");
2378
- } catch (err) {
2379
- if (err.code !== "ESRCH") {
2380
- slog2.warn(`failed to kill orphan pid ${pid}`, logger_default.errorData(err));
2381
- }
2382
- }
2383
- }
2384
- }
2385
- } catch {
2386
- try {
2387
- const portHex = port.toString(16).toUpperCase().padStart(4, "0");
2388
- const tcp6 = readFileSync4("/proc/net/tcp6", "utf-8");
2389
- for (const line of tcp6.split("\n")) {
2390
- if (!line.includes(`:${portHex} `)) continue;
2391
- const fields = line.trim().split(/\s+/);
2392
- if (fields[3] !== "0A") continue;
2393
- const inode = parseInt(fields[9], 10);
2394
- if (!inode) continue;
2395
- for (const pidDir of readdirSync("/proc").filter((f) => /^\d+$/.test(f))) {
2396
- try {
2397
- const fds = readdirSync(`/proc/${pidDir}/fd`);
2398
- for (const fd of fds) {
2399
- try {
2400
- const link = readlinkSync(`/proc/${pidDir}/fd/${fd}`);
2401
- if (link.includes(`socket:[${inode}]`)) {
2402
- process.kill(parseInt(pidDir, 10), "SIGTERM");
2403
- }
2404
- } catch {
2405
- }
2406
- }
2407
- } catch {
2408
- }
2409
- }
2410
- }
2411
- } catch (err) {
2412
- slog2.warn(`failed to kill orphan on port ${port} via /proc`, logger_default.errorData(err));
2413
- }
2414
- }
2415
- await new Promise((r) => setTimeout(r, 1e3));
2416
- }
2417
- onActivityEvent(event) {
2418
- const state = this.states.get(event.mind);
2419
- if (!state?.sleeping || !state.wokenByTrigger) return;
2420
- if (this.transitioning.has(event.mind)) return;
2421
- if (event.type === "mind_idle") {
2422
- slog2.info(`${event.mind} going back to sleep after trigger wake`);
2423
- state.wokenByTrigger = false;
2424
- this.transitioning.add(event.mind);
2425
- sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
2426
- state.sleeping = true;
2427
- state.sleepingSince = (/* @__PURE__ */ new Date()).toISOString();
2428
- const sleepConfig = this.getSleepConfig(event.mind);
2429
- state.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
2430
- this.saveState();
2431
- slog2.info(`${event.mind} returned to sleep`);
2432
- }).catch((err) => {
2433
- slog2.error(`failed to return ${event.mind} to sleep`, logger_default.errorData(err));
2434
- }).finally(() => {
2435
- this.transitioning.delete(event.mind);
2436
- });
2437
- }
2438
- }
2439
- };
2440
- var instance5 = null;
2441
- function initSleepManager() {
2442
- if (instance5) throw new Error("SleepManager already initialized");
2443
- instance5 = new SleepManager();
2444
- return instance5;
2445
- }
2446
- function getSleepManager() {
2447
- if (!instance5) throw new Error("SleepManager not initialized \u2014 call initSleepManager() first");
2448
- return instance5;
2449
- }
2450
- function getSleepManagerIfReady() {
2451
- return instance5;
2452
- }
2453
-
2454
- // src/lib/events/mind-events.ts
2455
- var subscribers = /* @__PURE__ */ new Map();
2456
- var globalSubscribers = /* @__PURE__ */ new Set();
2457
- function subscribe2(mind, callback) {
2458
- let set = subscribers.get(mind);
2459
- if (!set) {
2460
- set = /* @__PURE__ */ new Set();
2461
- subscribers.set(mind, set);
2462
- }
2463
- set.add(callback);
2464
- return () => {
2465
- set.delete(callback);
2466
- if (set.size === 0) subscribers.delete(mind);
2467
- };
2468
- }
2469
- function subscribeAll(callback) {
2470
- globalSubscribers.add(callback);
2471
- return () => {
2472
- globalSubscribers.delete(callback);
2473
- };
2474
- }
2475
- function publish3(mind, event) {
2476
- const set = subscribers.get(mind);
2477
- if (set) {
2478
- for (const cb of set) {
2479
- try {
2480
- cb(event);
2481
- } catch (err) {
2482
- logger_default.error(`[mind-events] subscriber threw for ${mind}`, logger_default.errorData(err));
2483
- set.delete(cb);
2484
- if (set.size === 0) subscribers.delete(mind);
2485
- }
2486
- }
2487
- }
2488
- for (const cb of globalSubscribers) {
2489
- try {
2490
- cb(event);
2491
- } catch (err) {
2492
- logger_default.error("[mind-events] global subscriber threw", logger_default.errorData(err));
2493
- globalSubscribers.delete(cb);
2494
- }
2495
- }
2496
- }
2497
-
2498
- // src/lib/delivery/delivery-manager.ts
2499
- import { readFile, realpath } from "fs/promises";
2500
- import { extname, resolve as resolve5 } from "path";
2501
- import { and as and2, eq as eq4, sql } from "drizzle-orm";
2502
-
2503
- // src/lib/typing.ts
2504
- var DEFAULT_TTL_MS = 1e4;
2505
- var SWEEP_INTERVAL_MS = 5e3;
2506
- var TypingMap = class {
2507
- channels = /* @__PURE__ */ new Map();
2508
- sweepTimer;
2509
- constructor() {
2510
- this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
2511
- this.sweepTimer.unref();
2512
- }
2513
- set(channel, sender, opts) {
2514
- const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
2515
- let senders = this.channels.get(channel);
2516
- if (!senders) {
2517
- senders = /* @__PURE__ */ new Map();
2518
- this.channels.set(channel, senders);
2519
- }
2520
- senders.set(sender, { expiresAt });
2521
- }
2522
- delete(channel, sender) {
2523
- const senders = this.channels.get(channel);
2524
- if (senders) {
2525
- senders.delete(sender);
2526
- if (senders.size === 0) {
2527
- this.channels.delete(channel);
2528
- }
2529
- }
2530
- }
2531
- /** Remove a sender from all channels (e.g. when a mind finishes processing). Returns affected channel names. */
2532
- deleteSender(sender) {
2533
- const affected = [];
2534
- for (const [channel, senders] of this.channels) {
2535
- if (senders.has(sender)) {
2536
- senders.delete(sender);
2537
- affected.push(channel);
2538
- }
2539
- if (senders.size === 0) {
2540
- this.channels.delete(channel);
2541
- }
2542
- }
2543
- return affected;
2544
- }
2545
- get(channel) {
2546
- const senders = this.channels.get(channel);
2547
- if (!senders) return [];
2548
- const now = Date.now();
2549
- const result = [];
2550
- for (const [sender, entry] of senders) {
2551
- if (entry.expiresAt > now) {
2552
- result.push(sender);
2553
- }
2554
- }
2555
- return result;
2556
- }
2557
- dispose() {
2558
- clearInterval(this.sweepTimer);
2559
- this.channels.clear();
2560
- if (instance6 === this) instance6 = void 0;
2561
- }
2562
- sweep() {
2563
- const now = Date.now();
2564
- for (const [channel, senders] of this.channels) {
2565
- for (const [sender, entry] of senders) {
2566
- if (entry.expiresAt <= now) {
2567
- senders.delete(sender);
2568
- }
2569
- }
2570
- if (senders.size === 0) {
2571
- this.channels.delete(channel);
2572
- }
2573
- }
2574
- }
2575
- };
2576
- var instance6;
2577
- function getTypingMap() {
2578
- if (!instance6) {
2579
- instance6 = new TypingMap();
2580
- }
2581
- return instance6;
2582
- }
2583
- function isConversationId(channel) {
2584
- return !channel.startsWith("@") && !channel.startsWith("#") && !channel.includes(":") && !channel.includes("/");
2585
- }
2586
- function publishTypingForChannels(channels, map) {
2587
- for (const channel of channels) {
2588
- if (isConversationId(channel)) {
2589
- publish2(channel, { type: "typing", senders: map.get(channel) });
2590
- }
2591
- }
2592
- }
2593
-
2594
- // src/lib/delivery/delivery-manager.ts
2595
- var dlog = logger_default.child("delivery-manager");
2596
- var MAX_BATCH_SIZE = 50;
2597
- var DeliveryManager = class {
2598
- sessionStates = /* @__PURE__ */ new Map();
2599
- batchBuffers = /* @__PURE__ */ new Map();
2600
- // --- Public API ---
2601
- /**
2602
- * Route and deliver a message to a mind. This is the main entry point.
2603
- * The message is routed via the mind's routes.json, then either delivered immediately
2604
- * or queued for batching depending on the session's delivery mode.
2605
- */
2606
- async routeAndDeliver(mindName, payload) {
2607
- const baseName = await getBaseName(mindName);
2608
- const config = getRoutingConfig(baseName);
2609
- if (payload.session) {
2610
- let sessionName2 = payload.session;
2611
- if (sessionName2 === "$new") {
2612
- sessionName2 = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2613
- }
2614
- const sessionConfig2 = resolveDeliveryMode(config, sessionName2);
2615
- if (sessionConfig2.delivery.mode === "batch") {
2616
- await this.enqueueBatch(mindName, sessionName2, payload, sessionConfig2);
2617
- return { routed: true, session: sessionName2, destination: "mind", mode: "batch" };
2618
- }
2619
- await this.deliverToMind(mindName, sessionName2, payload, sessionConfig2);
2620
- return { routed: true, session: sessionName2, destination: "mind", mode: "immediate" };
2621
- }
2622
- const meta = {
2623
- channel: payload.channel,
2624
- sender: payload.sender ?? void 0,
2625
- isDM: payload.isDM,
2626
- participantCount: payload.participantCount
2627
- };
2628
- const route = resolveRoute(config, meta);
2629
- dlog.debug(
2630
- `route for ${mindName} ch=${payload.channel}: dest=${route.destination} matched=${route.matched}`
2631
- );
2632
- if (route.destination === "file") {
2633
- return { routed: true, session: route.path, destination: "file", mode: "immediate" };
2634
- }
2635
- if (!route.matched && config.gateUnmatched !== false) {
2636
- dlog.debug(`gating unmatched channel ${payload.channel} for ${mindName}`);
2637
- await this.gateMessage(mindName, route.session, payload);
2638
- return { routed: true, session: route.session, destination: "mind", mode: "gated" };
2639
- }
2640
- if (route.mode === "mention" && payload.sender) {
2641
- const text = extractTextContent(payload.content);
2642
- const escaped = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2643
- const pattern = new RegExp(`\\b${escaped}\\b`, "i");
2644
- if (!pattern.test(text)) {
2645
- dlog.debug(`mention-filtered message on ${payload.channel} for ${mindName}`);
2646
- return { routed: false, reason: "mention-filtered" };
2647
- }
2648
- }
2649
- let sessionName = route.session;
2650
- if (sessionName === "$new") {
2651
- sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2652
- }
2653
- tagRecentInbound(baseName, sessionName, payload.channel).catch((err) => {
2654
- dlog.warn(`tagRecentInbound failed for ${baseName}`, logger_default.errorData(err));
2655
- });
2656
- const sessionConfig = resolveDeliveryMode(config, sessionName, route.rule);
2657
- if (sessionConfig.delivery.mode === "batch") {
2658
- dlog.debug(`enqueueing batch message for ${mindName}/${sessionName}`);
2659
- await this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
2660
- return { routed: true, session: sessionName, destination: "mind", mode: "batch" };
2661
- }
2662
- await this.deliverToMind(mindName, sessionName, payload, sessionConfig);
2663
- return { routed: true, session: sessionName, destination: "mind", mode: "immediate" };
2664
- }
2665
- /**
2666
- * Called when a mind's session emits a "done" event — decrements active count
2667
- * and may trigger batch flush if session goes idle.
2668
- *
2669
- * This method is intentionally synchronous to avoid race conditions: the caller
2670
- * has already resolved baseName, and any async yield here (e.g. getBaseName)
2671
- * would allow concurrent deliveries to incrementActive before the decrement runs,
2672
- * causing isSessionBusy to return true even when no deliveries are pending.
2673
- */
2674
- sessionDone(baseName, session) {
2675
- if (session) {
2676
- this.decrementActive(baseName, session);
2677
- } else {
2678
- const mindSessions = this.sessionStates.get(baseName);
2679
- if (mindSessions) {
2680
- for (const [sessionName] of mindSessions) {
2681
- this.decrementActive(baseName, sessionName);
2682
- }
2683
- }
2684
- }
2685
- }
2686
- /**
2687
- * Restore queued messages from DB on daemon restart.
2688
- */
2689
- async restoreFromDb() {
2690
- try {
2691
- const db = await getDb();
2692
- const rows = await db.select().from(deliveryQueue).where(eq4(deliveryQueue.status, "pending"));
2693
- for (const row of rows) {
2694
- let payload;
2695
- try {
2696
- payload = JSON.parse(row.payload);
2697
- } catch (parseErr) {
2698
- dlog.warn(
2699
- `corrupt payload in delivery queue row ${row.id}, skipping`,
2700
- logger_default.errorData(parseErr)
2701
- );
2702
- continue;
2703
- }
2704
- const config = getRoutingConfig(row.mind);
2705
- const sessionConfig = resolveDeliveryMode(config, row.session);
2706
- if (sessionConfig.delivery.mode === "batch") {
2707
- this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
2708
- } else {
2709
- try {
2710
- await db.delete(deliveryQueue).where(eq4(deliveryQueue.id, row.id));
2711
- } catch (err) {
2712
- dlog.warn(`failed to delete queue row ${row.id} for ${row.mind}`, logger_default.errorData(err));
2713
- }
2714
- this.deliverToMind(row.mind, row.session, payload, sessionConfig).catch((err) => {
2715
- dlog.warn(`failed to restore delivery for ${row.mind}`, logger_default.errorData(err));
2716
- });
2717
- }
2718
- }
2719
- if (rows.length > 0) {
2720
- dlog.info(`restored ${rows.length} queued messages from DB`);
2721
- }
2722
- } catch (err) {
2723
- dlog.warn("failed to restore delivery queue from DB", logger_default.errorData(err));
2724
- }
2725
- }
2726
- /**
2727
- * Get pending (gated) messages for a mind.
2728
- */
2729
- async getPending(mindName) {
2730
- const db = await getDb();
2731
- const rows = await db.select().from(deliveryQueue).where(and2(eq4(deliveryQueue.mind, mindName), eq4(deliveryQueue.status, "gated")));
2732
- const byChannel = /* @__PURE__ */ new Map();
2733
- for (const row of rows) {
2734
- const ch = row.channel ?? "unknown";
2735
- const existing = byChannel.get(ch) ?? [];
2736
- existing.push(row);
2737
- byChannel.set(ch, existing);
2738
- }
2739
- return [...byChannel.entries()].map(([channel, channelRows]) => {
2740
- const firstRow = channelRows[0];
2741
- const payload = JSON.parse(firstRow.payload);
2742
- const text = extractTextContent(payload.content);
2743
- return {
2744
- channel,
2745
- sender: firstRow.sender,
2746
- count: channelRows.length,
2747
- firstSeen: firstRow.created_at,
2748
- preview: text.length > 200 ? `${text.slice(0, 200)}...` : text
2749
- };
2750
- });
2751
- }
2752
- /**
2753
- * Check if a session is currently busy (has active deliveries).
2754
- */
2755
- isSessionBusy(mindName, session) {
2756
- const state = this.sessionStates.get(mindName)?.get(session);
2757
- return (state?.activeCount ?? 0) > 0;
2758
- }
2759
- /**
2760
- * Check if any session for a mind is currently busy.
2761
- */
2762
- isMindBusy(mindName) {
2763
- const mindSessions = this.sessionStates.get(mindName);
2764
- if (!mindSessions) return false;
2765
- for (const [, state] of mindSessions) {
2766
- if (state.activeCount > 0) return true;
2767
- }
2768
- return false;
2769
- }
2770
- /**
2771
- * Clear all session state for a specific mind (called on mind stop/crash).
2772
- * Resets active counts and cleans up batch buffers so ghost counts don't accumulate.
2773
- */
2774
- clearMindSessions(mindName) {
2775
- this.sessionStates.delete(mindName);
2776
- const toDelete = [];
2777
- for (const [bufferKey, buffer] of this.batchBuffers) {
2778
- if (bufferKey.startsWith(`${mindName}:`)) {
2779
- if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
2780
- if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
2781
- toDelete.push(bufferKey);
2782
- }
2783
- }
2784
- for (const k of toDelete) this.batchBuffers.delete(k);
2785
- }
2786
- /**
2787
- * Cleanup all timers and subscriptions.
2788
- */
2789
- dispose() {
2790
- for (const [, buffer] of this.batchBuffers) {
2791
- if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
2792
- if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
2793
- }
2794
- this.batchBuffers.clear();
2795
- this.sessionStates.clear();
2796
- if (instance7 === this) instance7 = void 0;
2797
- }
2798
- // --- Private ---
2799
- async resolvePort(mindName) {
2800
- const entry = await findMind(mindName);
2801
- if (!entry) return null;
2802
- const baseName = entry.parent ?? mindName;
2803
- return { baseName, port: entry.port };
2804
- }
2805
- async postToMind(port, body) {
2806
- const controller = new AbortController();
2807
- const timeout = setTimeout(() => controller.abort(), 12e4);
2808
- try {
2809
- const res = await fetch(`http://127.0.0.1:${port}/message`, {
2810
- method: "POST",
2811
- headers: { "Content-Type": "application/json" },
2812
- body,
2813
- signal: controller.signal
2814
- });
2815
- if (!res.ok) {
2816
- const text = await res.text().catch(() => "");
2817
- dlog.warn(`mind responded ${res.status}: ${text}`);
2818
- return false;
2819
- }
2820
- await res.text().catch(() => {
2821
- });
2822
- return true;
2823
- } finally {
2824
- clearTimeout(timeout);
2825
- }
2826
- }
2827
- async deliverToMind(mindName, session, payload, sessionConfig) {
2828
- const resolved = await this.resolvePort(mindName);
2829
- if (!resolved) {
2830
- dlog.warn(`cannot deliver to ${mindName}: mind not found`);
2831
- return;
2832
- }
2833
- const { baseName, port } = resolved;
2834
- const senders = /* @__PURE__ */ new Set();
2835
- if (payload.sender) senders.add(payload.sender);
2836
- const channels = /* @__PURE__ */ new Set();
2837
- if (payload.channel) channels.add(payload.channel);
2838
- this.incrementActive(baseName, session, senders, channels);
2839
- const typingMap = getTypingMap();
2840
- if (payload.channel) {
2841
- typingMap.set(payload.channel, baseName, { persistent: true });
2842
- }
2843
- if (payload.conversationId) {
2844
- typingMap.set(payload.conversationId, baseName, { persistent: true });
2845
- }
2846
- const enrichedPayload = await this.enrichWithProfiles(baseName, session, payload);
2847
- const body = JSON.stringify({
2848
- ...enrichedPayload,
2849
- session,
2850
- interrupt: sessionConfig.interrupt,
2851
- instructions: sessionConfig.instructions
2852
- });
2853
- try {
2854
- const ok = await this.postToMind(port, body);
2855
- if (!ok) {
2856
- this.decrementActive(baseName, session);
2857
- publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
2858
- }
2859
- } catch (err) {
2860
- dlog.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
2861
- this.decrementActive(baseName, session);
2862
- publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
2863
- }
2864
- }
2865
- async deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride) {
2866
- const resolved = await this.resolvePort(mindName);
2867
- if (!resolved) {
2868
- dlog.warn(`cannot deliver batch to ${mindName}: mind not found`);
2869
- return;
2870
- }
2871
- const { baseName, port } = resolved;
2872
- const enrichedMessages = await Promise.all(
2873
- messages2.map(async (msg, i) => {
2874
- const isFirst = messages2.findIndex((m) => m.channel === msg.channel) === i;
2875
- if (!isFirst) return msg;
2876
- const enrichedPayload = await this.enrichWithProfiles(baseName, session, msg.payload);
2877
- return { ...msg, payload: enrichedPayload };
2878
- })
2879
- );
2880
- const channels = {};
2881
- for (const msg of enrichedMessages) {
2882
- const ch = msg.channel ?? "unknown";
2883
- if (!channels[ch]) channels[ch] = [];
2884
- channels[ch].push(msg.payload);
2885
- }
2886
- const senders = /* @__PURE__ */ new Set();
2887
- const channelSet = /* @__PURE__ */ new Set();
2888
- for (const msg of messages2) {
2889
- if (msg.sender) senders.add(msg.sender);
2890
- if (msg.channel) channelSet.add(msg.channel);
2891
- }
2892
- this.incrementActive(baseName, session, senders, channelSet);
2893
- const typingMap = getTypingMap();
2894
- for (const ch of Object.keys(channels)) {
2895
- if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
2896
- }
2897
- const seenConvIds = /* @__PURE__ */ new Set();
2898
- for (const msg of messages2) {
2899
- if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
2900
- seenConvIds.add(msg.payload.conversationId);
2901
- typingMap.set(msg.payload.conversationId, baseName, { persistent: true });
2902
- }
2903
- }
2904
- const body = JSON.stringify({
2905
- session,
2906
- batch: { channels },
2907
- interrupt: interruptOverride ?? sessionConfig.interrupt,
2908
- instructions: sessionConfig.instructions
2909
- });
2910
- try {
2911
- const ok = await this.postToMind(port, body);
2912
- if (!ok) {
2913
- this.decrementActive(baseName, session);
2914
- publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
2915
- } else {
2916
- try {
2917
- const db = await getDb();
2918
- await db.delete(deliveryQueue).where(
2919
- and2(
2920
- eq4(deliveryQueue.mind, baseName),
2921
- eq4(deliveryQueue.session, session),
2922
- eq4(deliveryQueue.status, "pending")
2923
- )
2924
- );
2925
- } catch (err) {
2926
- dlog.warn(
2927
- `failed to clean delivery queue for ${baseName}/${session}`,
2928
- logger_default.errorData(err)
2929
- );
2930
- }
2931
- }
2932
- } catch (err) {
2933
- dlog.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
2934
- this.decrementActive(baseName, session);
2935
- publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
2936
- }
2937
- }
2938
- async enqueueBatch(mindName, session, payload, sessionConfig) {
2939
- const delivery = sessionConfig.delivery;
2940
- if (delivery.triggers?.length) {
2941
- const text = extractTextContent(payload.content);
2942
- const lower = text.toLowerCase();
2943
- if (delivery.triggers.some((t) => lower.includes(t.toLowerCase()))) {
2944
- await this.flushBatch(mindName, session, [
2945
- {
2946
- payload,
2947
- channel: payload.channel,
2948
- sender: payload.sender ?? null,
2949
- createdAt: Date.now()
2950
- }
2951
- ]);
2952
- return;
2953
- }
2954
- }
2955
- const baseName = await getBaseName(mindName);
2956
- const state = this.sessionStates.get(baseName)?.get(session);
2957
- if (state && state.activeCount > 0 && payload.sender && !state.lastDeliverySenders.has(payload.sender) && payload.channel && state.lastDeliveryChannels.has(payload.channel) && Date.now() - state.lastDeliveredAt < delivery.maxWait * 1e3 && Date.now() - state.lastInterruptAt > delivery.debounce * 1e3) {
2958
- state.lastInterruptAt = Date.now();
2959
- this.persistToQueue(mindName, session, payload).catch((err) => {
2960
- dlog.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
2961
- });
2962
- await this.flushBatch(
2963
- mindName,
2964
- session,
2965
- [{ payload, channel: payload.channel, sender: payload.sender, createdAt: Date.now() }],
2966
- true
2967
- );
2968
- return;
2969
- }
2970
- this.persistToQueue(mindName, session, payload).catch((err) => {
2971
- dlog.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
2972
- });
2973
- this.addToBatchBuffer(mindName, session, payload, sessionConfig);
2974
- }
2975
- addToBatchBuffer(mindName, session, payload, sessionConfig) {
2976
- const delivery = sessionConfig.delivery;
2977
- const bufferKey = `${mindName}:${session}`;
2978
- let buffer = this.batchBuffers.get(bufferKey);
2979
- if (!buffer) {
2980
- buffer = {
2981
- messages: [],
2982
- debounceTimer: null,
2983
- maxWaitTimer: null,
2984
- delivery
2985
- };
2986
- this.batchBuffers.set(bufferKey, buffer);
2987
- }
2988
- buffer.messages.push({
2989
- payload,
2990
- channel: payload.channel,
2991
- sender: payload.sender ?? null,
2992
- createdAt: Date.now()
2993
- });
2994
- if (buffer.messages.length >= MAX_BATCH_SIZE) {
2995
- this.flushBatch(mindName, session);
2996
- return;
2997
- }
2998
- this.scheduleBatchTimers(mindName, session, bufferKey);
2999
- }
3000
- scheduleBatchTimers(mindName, session, bufferKey) {
3001
- const buffer = this.batchBuffers.get(bufferKey);
3002
- if (!buffer) return;
3003
- if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
3004
- buffer.debounceTimer = setTimeout(() => {
3005
- if (!this.isSessionBusy(mindName, session)) {
3006
- this.flushBatch(mindName, session);
3007
- }
3008
- }, buffer.delivery.debounce * 1e3);
3009
- buffer.debounceTimer.unref();
3010
- if (!buffer.maxWaitTimer) {
3011
- buffer.maxWaitTimer = setTimeout(() => {
3012
- this.flushBatch(mindName, session);
3013
- }, buffer.delivery.maxWait * 1e3);
3014
- buffer.maxWaitTimer.unref();
3015
- }
3016
- }
3017
- async flushBatch(mindName, session, extra, interruptOverride) {
3018
- const bufferKey = `${mindName}:${session}`;
3019
- const buffer = this.batchBuffers.get(bufferKey);
3020
- const messages2 = [];
3021
- if (buffer) {
3022
- if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
3023
- if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
3024
- buffer.debounceTimer = null;
3025
- buffer.maxWaitTimer = null;
3026
- messages2.push(...buffer.messages.splice(0));
3027
- this.batchBuffers.delete(bufferKey);
3028
- }
3029
- if (extra) messages2.push(...extra);
3030
- if (messages2.length === 0) return;
3031
- const baseName = await getBaseName(mindName);
3032
- const config = getRoutingConfig(baseName);
3033
- const sessionConfig = resolveDeliveryMode(config, session);
3034
- dlog.info(
3035
- `flushing batch for ${mindName}/${session}: ${messages2.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
3036
- );
3037
- this.deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride).catch(
3038
- (err) => {
3039
- dlog.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
3040
- }
3041
- );
3042
- }
3043
- async gateMessage(mindName, session, payload) {
3044
- const baseName = await getBaseName(mindName);
3045
- await this.persistToQueue(baseName, session, payload, "gated");
3046
- try {
3047
- const db = await getDb();
3048
- const count = await db.select({ count: sql`count(*)` }).from(deliveryQueue).where(
3049
- and2(
3050
- eq4(deliveryQueue.mind, baseName),
3051
- eq4(deliveryQueue.channel, payload.channel),
3052
- eq4(deliveryQueue.status, "gated")
3053
- )
3054
- );
3055
- if ((count[0]?.count ?? 0) <= 1) {
3056
- await this.sendInviteNotification(mindName, payload);
3057
- }
3058
- } catch (err) {
3059
- dlog.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
3060
- }
3061
- }
3062
- async sendInviteNotification(mindName, payload) {
3063
- const text = extractTextContent(payload.content);
3064
- const preview = text.length > 200 ? `${text.slice(0, 200)}...` : text;
3065
- const channel = payload.channel ?? "unknown";
3066
- const notification = [
3067
- `[New channel: ${channel}]`,
3068
- `Sender: ${payload.sender ?? "unknown"}`,
3069
- payload.platform ? `Platform: ${payload.platform}` : null,
3070
- payload.participantCount ? `Participants: ${payload.participantCount}` : null,
3071
- "",
3072
- `Preview: ${preview}`,
3073
- "",
3074
- `To accept this channel, add a routing rule for "${channel}" to your routes.json.`,
3075
- `Messages are being held until a route is configured.`
3076
- ].filter((line) => line !== null).join("\n");
3077
- const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-TYLOL7SX.js");
3078
- await sendSystemMessage2(mindName, notification);
3079
- }
3080
- async persistToQueue(mindName, session, payload, status = "pending") {
3081
- try {
3082
- const db = await getDb();
3083
- await db.insert(deliveryQueue).values({
3084
- mind: mindName,
3085
- session,
3086
- channel: payload.channel ?? null,
3087
- sender: payload.sender ?? null,
3088
- status,
3089
- payload: JSON.stringify(payload)
3090
- });
3091
- } catch (err) {
3092
- dlog.warn(
3093
- `failed to persist to delivery queue for ${mindName}/${session}`,
3094
- logger_default.errorData(err)
3095
- );
3096
- }
3097
- }
3098
- async enrichWithProfiles(mindName, session, payload) {
3099
- if (!payload.conversationId || !payload.channel) return payload;
3100
- const mindSessions = this.sessionStates.get(mindName);
3101
- const state = mindSessions?.get(session);
3102
- if (!state) return payload;
3103
- const channelKey = payload.channel;
3104
- if (state.seenChannelProfiles.has(channelKey)) return payload;
3105
- try {
3106
- const participants = await getParticipants(payload.conversationId);
3107
- const profiles = participants.map((p) => ({
3108
- username: p.username,
3109
- userType: p.userType,
3110
- displayName: p.displayName,
3111
- description: p.description
3112
- }));
3113
- const avatarBlocks = await this.loadAvatarBlocks(participants);
3114
- state.seenChannelProfiles.add(channelKey);
3115
- const enriched = { ...payload, participantProfiles: profiles };
3116
- if (avatarBlocks.length > 0) {
3117
- const existing = Array.isArray(payload.content) ? payload.content : typeof payload.content === "string" ? [{ type: "text", text: payload.content }] : [];
3118
- enriched.content = [...avatarBlocks, ...existing];
3119
- }
3120
- return enriched;
3121
- } catch (err) {
3122
- dlog.warn(`failed to fetch participant profiles for ${mindName}`, logger_default.errorData(err));
3123
- return payload;
3124
- }
3125
- }
3126
- async loadAvatarBlocks(participants) {
3127
- const blocks = [];
3128
- for (const p of participants) {
3129
- if (!p.avatar) continue;
3130
- try {
3131
- let filePath;
3132
- if (p.userType === "mind") {
3133
- const dir = mindDir(p.username);
3134
- const config = readVoluteConfig(dir);
3135
- if (!config?.profile?.avatar) continue;
3136
- filePath = resolve5(dir, "home", config.profile.avatar);
3137
- const homeDir = resolve5(dir, "home");
3138
- if (!filePath.startsWith(`${homeDir}/`)) {
3139
- dlog.warn(`avatar path for ${p.username} escapes home directory, skipping`);
3140
- continue;
3141
- }
3142
- try {
3143
- const realHome = await realpath(homeDir);
3144
- const realAvatar = await realpath(filePath);
3145
- if (!realAvatar.startsWith(`${realHome}/`)) {
3146
- dlog.warn(
3147
- `avatar symlink for ${p.username} resolves outside home directory, skipping`
3148
- );
3149
- continue;
3150
- }
3151
- } catch (err) {
3152
- if (err.code === "ENOENT") continue;
3153
- throw err;
3154
- }
3155
- } else {
3156
- filePath = resolve5(voluteHome(), "avatars", p.avatar);
3157
- }
3158
- const ext = extname(filePath).toLowerCase();
3159
- const mimeMap = {
3160
- ".png": "image/png",
3161
- ".jpg": "image/jpeg",
3162
- ".jpeg": "image/jpeg",
3163
- ".gif": "image/gif",
3164
- ".webp": "image/webp"
3165
- };
3166
- const mediaType = mimeMap[ext];
3167
- if (!mediaType) continue;
3168
- const data = await readFile(filePath);
3169
- let imageData = data;
3170
- try {
3171
- const sharpMod = await import("./lib-DYEZMGW7.js");
3172
- imageData = await sharpMod.default(data).resize(128, 128, { fit: "cover" }).toBuffer();
3173
- } catch (err) {
3174
- const code = err.code;
3175
- if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
3176
- dlog.debug("sharp not available, sending full-size avatar");
3177
- } else {
3178
- dlog.warn(
3179
- `avatar resize failed for ${p.username}, sending original`,
3180
- logger_default.errorData(err)
3181
- );
3182
- }
3183
- }
3184
- blocks.push(
3185
- { type: "text", text: `[Avatar for ${p.username}]` },
3186
- { type: "image", media_type: mediaType, data: imageData.toString("base64") }
3187
- );
3188
- } catch (err) {
3189
- const code = err.code;
3190
- if (code !== "ENOENT") {
3191
- dlog.warn(`failed to load avatar for ${p.username}`, logger_default.errorData(err));
3192
- }
3193
- }
3194
- }
3195
- return blocks;
3196
- }
3197
- incrementActive(mind, session, senders, channels) {
3198
- let mindSessions = this.sessionStates.get(mind);
3199
- if (!mindSessions) {
3200
- mindSessions = /* @__PURE__ */ new Map();
3201
- this.sessionStates.set(mind, mindSessions);
3202
- }
3203
- const state = mindSessions.get(session) ?? {
3204
- activeCount: 0,
3205
- lastDeliveredAt: 0,
3206
- lastDeliverySenders: /* @__PURE__ */ new Set(),
3207
- lastDeliveryChannels: /* @__PURE__ */ new Set(),
3208
- lastInterruptAt: 0,
3209
- seenChannelProfiles: /* @__PURE__ */ new Set()
3210
- };
3211
- state.activeCount++;
3212
- state.lastDeliveredAt = Date.now();
3213
- if (senders) state.lastDeliverySenders = senders;
3214
- if (channels) state.lastDeliveryChannels = channels;
3215
- mindSessions.set(session, state);
3216
- }
3217
- decrementActive(mind, session) {
3218
- const mindSessions = this.sessionStates.get(mind);
3219
- if (!mindSessions) return;
3220
- const state = mindSessions.get(session);
3221
- if (!state) return;
3222
- state.activeCount = Math.max(0, state.activeCount - 1);
3223
- if (state.activeCount === 0) {
3224
- const bufferKey = `${mind}:${session}`;
3225
- const buffer = this.batchBuffers.get(bufferKey);
3226
- if (buffer && buffer.messages.length > 0) {
3227
- this.scheduleBatchTimers(mind, session, bufferKey);
3228
- }
3229
- }
3230
- }
3231
- };
3232
- var instance7;
3233
- function initDeliveryManager() {
3234
- if (instance7) throw new Error("DeliveryManager already initialized");
3235
- instance7 = new DeliveryManager();
3236
- return instance7;
3237
- }
3238
- function getDeliveryManager() {
3239
- if (!instance7) {
3240
- throw new Error("DeliveryManager not initialized \u2014 call initDeliveryManager() first");
3241
- }
3242
- return instance7;
3243
- }
3244
-
3245
- // src/lib/delivery/message-delivery.ts
3246
- var dlog2 = logger_default.child("delivery");
3247
- async function recordInbound(mind, channel, sender, content) {
3248
- let insertedId;
3249
- try {
3250
- const db = await getDb();
3251
- const result = await db.insert(mindHistory).values({
3252
- mind,
3253
- type: "inbound",
3254
- channel,
3255
- sender,
3256
- content
3257
- }).returning({ id: mindHistory.id });
3258
- insertedId = result[0]?.id;
3259
- } catch (err) {
3260
- dlog2.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
3261
- }
3262
- publish3(mind, {
3263
- mind,
3264
- type: "inbound",
3265
- channel,
3266
- content: content ?? void 0,
3267
- sender: sender ?? void 0
3268
- });
3269
- return insertedId;
3270
- }
3271
- async function recordOutbound(mind, channel, content, opts = {}) {
3272
- try {
3273
- const db = await getDb();
3274
- const result = await db.insert(mindHistory).values({
3275
- mind,
3276
- type: "outbound",
3277
- channel,
3278
- content,
3279
- turn_id: null,
3280
- message_id: opts.messageId ?? null
3281
- }).returning({ id: mindHistory.id });
3282
- return result[0]?.id;
3283
- } catch (err) {
3284
- dlog2.warn(`failed to persist outbound for ${mind}`, logger_default.errorData(err));
3285
- return void 0;
3286
- }
3287
- }
3288
- var OUTBOUND_MARKER_RE = /\[volute:outbound:(\d+)\]/g;
3289
- var ACTIVITY_MARKER_RE = /\[volute:activity:(\d+)\]/g;
3290
- async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEventId) {
3291
- if (!toolResultContent) return;
3292
- const db = await getDb();
3293
- for (const match of toolResultContent.matchAll(OUTBOUND_MARKER_RE)) {
3294
- const outboundId = Number(match[1]);
3295
- try {
3296
- const rows = await db.select({
3297
- id: mindHistory.id,
3298
- channel: mindHistory.channel,
3299
- content: mindHistory.content,
3300
- message_id: mindHistory.message_id
3301
- }).from(mindHistory).where(and3(eq5(mindHistory.id, outboundId), eq5(mindHistory.mind, mind))).limit(1);
3302
- const row = rows[0];
3303
- if (!row) {
3304
- dlog2.warn(`outbound marker references missing record: mind=${mind} id=${outboundId}`);
3305
- continue;
3306
- }
3307
- await db.update(mindHistory).set({ turn_id: turnId }).where(eq5(mindHistory.id, outboundId));
3308
- if (row.message_id) {
3309
- await db.update(messages).set({
3310
- turn_id: turnId,
3311
- ...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
3312
- }).where(eq5(messages.id, Number(row.message_id)));
3313
- }
3314
- publish3(mind, {
3315
- mind,
3316
- type: "outbound",
3317
- channel: row.channel ?? void 0,
3318
- content: row.content ?? void 0,
3319
- turnId
3320
- });
3321
- } catch (err) {
3322
- dlog2.warn(`failed to link outbound ${outboundId} to turn ${turnId}`, logger_default.errorData(err));
3323
- }
3324
- }
3325
- const activityIds = [];
3326
- for (const match of toolResultContent.matchAll(ACTIVITY_MARKER_RE)) {
3327
- activityIds.push(Number(match[1]));
3328
- }
3329
- if (activityIds.length > 0) {
3330
- try {
3331
- await db.update(activity).set({
3332
- turn_id: turnId,
3333
- ...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
3334
- }).where(inArray2(activity.id, activityIds));
3335
- const actRows = await db.select().from(activity).where(inArray2(activity.id, activityIds));
3336
- if (actRows.length > 0) {
3337
- await db.insert(mindHistory).values(
3338
- actRows.map((a) => ({
3339
- mind,
3340
- type: "activity",
3341
- content: a.summary,
3342
- metadata: a.metadata,
3343
- turn_id: turnId,
3344
- created_at: a.created_at
3345
- }))
3346
- );
3347
- }
3348
- } catch (err) {
3349
- dlog2.warn(`failed to link activities to turn ${turnId}`, logger_default.errorData(err));
3350
- }
3351
- }
3352
- }
3353
- async function tagUntaggedOutbound(mind, turnId) {
3354
- const db = await getDb();
3355
- const range = await db.select({
3356
- minId: sql2`MIN(${mindHistory.id})`,
3357
- maxId: sql2`MAX(${mindHistory.id})`
3358
- }).from(mindHistory).where(and3(eq5(mindHistory.mind, mind), eq5(mindHistory.turn_id, turnId)));
3359
- const minId = range[0]?.minId;
3360
- const maxId = range[0]?.maxId;
3361
- if (minId == null || maxId == null) return;
3362
- const orphans = await db.select({ id: mindHistory.id, message_id: mindHistory.message_id }).from(mindHistory).where(
3363
- and3(
3364
- eq5(mindHistory.mind, mind),
3365
- eq5(mindHistory.type, "outbound"),
3366
- sql2`${mindHistory.turn_id} IS NULL`,
3367
- sql2`${mindHistory.id} >= ${minId}`,
3368
- sql2`${mindHistory.id} <= ${maxId}`
3369
- )
3370
- );
3371
- if (orphans.length === 0) return;
3372
- const orphanIds = orphans.map((r) => r.id);
3373
- await db.update(mindHistory).set({ turn_id: turnId }).where(inArray2(mindHistory.id, orphanIds));
3374
- for (const orphan of orphans) {
3375
- if (!orphan.message_id) continue;
3376
- const toolUse = await db.select({ id: mindHistory.id }).from(mindHistory).where(
3377
- and3(
3378
- eq5(mindHistory.mind, mind),
3379
- eq5(mindHistory.turn_id, turnId),
3380
- eq5(mindHistory.type, "tool_use"),
3381
- sql2`${mindHistory.id} < ${orphan.id}`
3382
- )
3383
- ).orderBy(desc(mindHistory.id)).limit(1);
3384
- const sourceEventId = toolUse[0]?.id ?? null;
3385
- await db.update(messages).set({
3386
- turn_id: turnId,
3387
- ...sourceEventId != null ? { source_event_id: sourceEventId } : {}
3388
- }).where(eq5(messages.id, Number(orphan.message_id)));
3389
- }
3390
- dlog2.info(`tagged ${orphans.length} orphaned outbound record(s) for ${mind} with turn ${turnId}`);
3391
- }
3392
- async function tagUntaggedInbound(mind, turnId, {
3393
- limit = 5,
3394
- setTrigger = false,
3395
- channel
3396
- } = {}) {
3397
- const db = await getDb();
3398
- if (channel) {
3399
- const historyConditions = [
3400
- eq5(mindHistory.mind, mind),
3401
- eq5(mindHistory.type, "inbound"),
3402
- sql2`${mindHistory.turn_id} IS NULL`,
3403
- sql2`${mindHistory.created_at} > datetime('now', '-60 seconds')`,
3404
- eq5(mindHistory.channel, channel)
3405
- ];
3406
- const recentInbounds = await db.select({ id: mindHistory.id }).from(mindHistory).where(and3(...historyConditions)).orderBy(desc(mindHistory.id)).limit(limit);
3407
- if (recentInbounds.length > 0) {
3408
- const ids = recentInbounds.map((r) => r.id);
3409
- await db.update(mindHistory).set({ turn_id: turnId }).where(inArray2(mindHistory.id, ids));
3410
- if (setTrigger) {
3411
- await db.update(turns).set({ trigger_event_id: recentInbounds[0].id }).where(eq5(turns.id, turnId));
3412
- }
3413
- }
3414
- }
3415
- const recentMsgs = await db.select({ id: messages.id }).from(messages).innerJoin(conversations, eq5(messages.conversation_id, conversations.id)).where(
3416
- and3(
3417
- eq5(conversations.mind_name, mind),
3418
- sql2`${messages.turn_id} IS NULL`,
3419
- sql2`${messages.sender_name} != ${mind}`,
3420
- sql2`${messages.created_at} > datetime('now', '-60 seconds')`
3421
- )
3422
- ).orderBy(desc(messages.id)).limit(limit);
3423
- if (recentMsgs.length > 0) {
3424
- const ids = recentMsgs.map((r) => r.id);
3425
- await db.update(messages).set({ turn_id: turnId }).where(inArray2(messages.id, ids));
3426
- }
3427
- }
3428
- async function tagRecentInbound(mind, session, channel) {
3429
- const turnId = getActiveTurnId(mind, session);
3430
- if (!turnId) return;
3431
- try {
3432
- await tagUntaggedInbound(mind, turnId, { limit: 1, channel });
3433
- } catch (err) {
3434
- dlog2.warn(`failed to tag recent inbound for ${mind} with turn ${turnId}`, logger_default.errorData(err));
3435
- }
3436
- }
3437
- function resolveSleepAction(sleepBehavior, wokenByTrigger, wakeTriggerMatches) {
3438
- if (sleepBehavior === "skip") return "skip";
3439
- if (sleepBehavior === "trigger-wake" && !wokenByTrigger) return "queue-and-wake";
3440
- if (!sleepBehavior && wakeTriggerMatches) return "queue-and-wake";
3441
- return "queue";
3442
- }
3443
- async function deliverMessage(mindName, payload) {
3444
- try {
3445
- const baseName = await getBaseName(mindName);
3446
- const entry = await findMind(baseName);
3447
- if (!entry) {
3448
- dlog2.warn(`cannot deliver to ${mindName}: mind not found`);
3449
- return;
3450
- }
3451
- const textContent = extractTextContent(payload.content);
3452
- await recordInbound(baseName, payload.channel, payload.sender ?? null, textContent);
3453
- const sleepManager = getSleepManagerIfReady();
3454
- if (sleepManager?.isSleeping(baseName)) {
3455
- const sleepState = sleepManager.getState(baseName);
3456
- const action = resolveSleepAction(
3457
- payload.whileSleeping,
3458
- sleepState.wokenByTrigger,
3459
- sleepManager.checkWakeTrigger(baseName, payload)
3460
- );
3461
- if (action === "skip") {
3462
- dlog2.info(
3463
- `skipped delivery to ${baseName} (sleeping, whileSleeping=skip, channel=${payload.channel})`
3464
- );
3465
- return;
3466
- }
3467
- await sleepManager.queueSleepMessage(baseName, payload);
3468
- if (action === "queue-and-wake") {
3469
- sleepManager.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch((err) => dlog2.warn(`failed to trigger-wake ${baseName}`, logger_default.errorData(err)));
3470
- }
3471
- return;
3472
- }
3473
- const manager = getDeliveryManager();
3474
- await manager.routeAndDeliver(mindName, payload);
3475
- } catch (err) {
3476
- dlog2.warn(`unexpected error delivering to ${mindName}`, logger_default.errorData(err));
3477
- }
3478
- }
3479
-
3480
- // src/lib/system-chat.ts
3481
- var slog3 = logger_default.child("system-chat");
3482
- var dmCache = /* @__PURE__ */ new Map();
3483
- function resetSystemDMCache() {
3484
- dmCache.clear();
3485
- }
3486
- async function ensureSystemDM(mindName) {
3487
- const cached = dmCache.get(mindName);
3488
- if (cached) return { conversationId: cached };
3489
- const systemUser = await getOrCreateSystemUser();
3490
- const mindUser = await getOrCreateMindUser(mindName);
3491
- if (systemUser.id === mindUser.id) {
3492
- throw new Error(`Cannot create system DM: mind "${mindName}" is the system user`);
3493
- }
3494
- const existing = await findDMConversation(mindName, [systemUser.id, mindUser.id]);
3495
- if (existing) {
3496
- dmCache.set(mindName, existing);
3497
- return { conversationId: existing };
3498
- }
3499
- const conv = await createConversation(mindName, "volute", {
3500
- participantIds: [systemUser.id, mindUser.id],
3501
- title: "Volute"
3502
- });
3503
- dmCache.set(mindName, conv.id);
3504
- return { conversationId: conv.id };
3505
- }
3506
- async function sendSystemMessage(mindName, text, opts) {
3507
- const isSpirit = mindName === "volute";
3508
- let conversationId;
3509
- if (!isSpirit) {
3510
- const dm = await ensureSystemDM(mindName);
3511
- conversationId = dm.conversationId;
3512
- await addMessage(conversationId, "user", "volute", [{ type: "text", text }]);
3513
- }
3514
- await deliverMessage(mindName, {
3515
- content: [{ type: "text", text }],
3516
- channel: "@volute",
3517
- ...conversationId ? { conversationId } : {},
3518
- sender: "volute",
3519
- isDM: true,
3520
- participants: ["volute", mindName],
3521
- participantCount: 2,
3522
- ...opts?.whileSleeping ? { whileSleeping: opts.whileSleeping } : {},
3523
- ...opts?.session ? { session: opts.session } : {}
3524
- });
3525
- }
3526
- async function sendSystemMessageDirect(mindName, text) {
3527
- const { conversationId } = await ensureSystemDM(mindName);
3528
- await addMessage(conversationId, "user", "volute", [{ type: "text", text }]);
3529
- await recordInbound(mindName, "@volute", "volute", text);
3530
- return { conversationId };
3531
- }
3532
- async function isSpiritAvailable() {
3533
- const spiritEntry = await findMind("volute");
3534
- return !!(spiritEntry?.running && spiritEntry.mindType === "spirit");
3535
- }
3536
- async function generateSystemReply(conversationId, mindName, message) {
3537
- if (await isSpiritAvailable()) {
3538
- try {
3539
- await deliverMessage("volute", {
3540
- content: [{ type: "text", text: message }],
3541
- channel: `@${mindName}`,
3542
- conversationId,
3543
- sender: mindName,
3544
- isDM: true,
3545
- participants: ["volute", mindName],
3546
- participantCount: 2
3547
- });
3548
- return;
3549
- } catch (err) {
3550
- slog3.warn(`failed to route to spirit, falling back to aiCompleteUtility`, logger_default.errorData(err));
3551
- }
3552
- }
3553
- const entry = await findMind(mindName);
3554
- const dir = mindDir(mindName);
3555
- const config = readVoluteConfig(dir);
3556
- const contextParts = [
3557
- "You are Volute, the system that manages this mind's infrastructure.",
3558
- "You are having a direct conversation with a mind. Be helpful, concise, and informative.",
3559
- `Mind name: ${mindName}`,
3560
- `Status: ${entry?.running ? "running" : "stopped"}`
3561
- ];
3562
- if (config?.model) contextParts.push(`Model: ${config.model}`);
3563
- if (config?.tokenBudget) contextParts.push(`Token budget: ${config.tokenBudget}`);
3564
- if (config?.sleep?.enabled) {
3565
- contextParts.push(`Sleep schedule: enabled`);
3566
- if (config.sleep.schedule?.sleep)
3567
- contextParts.push(`Sleep cron: ${config.sleep.schedule.sleep}`);
3568
- if (config.sleep.schedule?.wake) contextParts.push(`Wake cron: ${config.sleep.schedule.wake}`);
3569
- }
3570
- try {
3571
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-BJK2ROPX.js");
3572
- const sm = getSleepManagerIfReady2();
3573
- if (sm) {
3574
- const state = sm.getState(mindName);
3575
- if (state.sleeping) {
3576
- contextParts.push(`Sleep state: sleeping since ${state.sleepingSince}`);
3577
- }
3578
- }
3579
- } catch (err) {
3580
- slog3.debug("could not retrieve sleep state for system reply", logger_default.errorData(err));
3581
- }
3582
- try {
3583
- const schedules = config?.schedules;
3584
- if (schedules && schedules.length > 0) {
3585
- const activeSchedules = schedules.filter((s) => s.enabled !== false);
3586
- if (activeSchedules.length > 0) {
3587
- contextParts.push(
3588
- `Active schedules: ${activeSchedules.map((s) => `${s.id} (${s.cron ?? s.fireAt ?? "unknown"})`).join(", ")}`
3589
- );
3590
- }
3591
- }
3592
- } catch (err) {
3593
- slog3.debug("could not retrieve schedules for system reply", logger_default.errorData(err));
3594
- }
3595
- const systemPrompt = contextParts.join("\n");
3596
- const response = await aiCompleteUtility(systemPrompt, message);
3597
- if (!response) {
3598
- slog3.warn(`no AI model available for system reply to ${mindName}`);
3599
- const fallback = "I can't reply right now \u2014 no AI model is configured for system responses. An admin can set one up in Settings.";
3600
- await addMessage(conversationId, "assistant", "volute", [{ type: "text", text: fallback }]);
3601
- return;
3602
- }
3603
- await addMessage(conversationId, "assistant", "volute", [{ type: "text", text: response }]);
3604
- await deliverMessage(mindName, {
3605
- content: [{ type: "text", text: response }],
3606
- channel: "@volute",
3607
- conversationId,
3608
- sender: "volute",
3609
- isDM: true,
3610
- participants: ["volute", mindName],
3611
- participantCount: 2
3612
- });
3613
- }
3614
-
3615
- export {
3616
- PROMPT_KEYS,
3617
- PROMPT_DEFAULTS,
3618
- substitute,
3619
- getPrompt,
3620
- getPromptIfCustom,
3621
- getMindPromptDefaults,
3622
- resetSystemDMCache,
3623
- ensureSystemDM,
3624
- sendSystemMessage,
3625
- sendSystemMessageDirect,
3626
- generateSystemReply,
3627
- RotatingLog,
3628
- resolveMindToken,
3629
- RestartTracker,
3630
- createTurn,
3631
- getActiveTurnId,
3632
- trackToolUse,
3633
- getLastToolUseEventId,
3634
- assignSession,
3635
- completeTurn,
3636
- completeOrphanedTurns,
3637
- getTypingMap,
3638
- isConversationId,
3639
- publishTypingForChannels,
3640
- DeliveryManager,
3641
- initDeliveryManager,
3642
- getDeliveryManager,
3643
- MindManager,
3644
- initMindManager,
3645
- getMindManager,
3646
- initMailPoller,
3647
- Scheduler,
3648
- initScheduler,
3649
- getScheduler,
3650
- initTokenBudget,
3651
- getTokenBudget,
3652
- startMindFull,
3653
- sleepMind,
3654
- wakeMind,
3655
- startSpiritFull,
3656
- stopSpiritFull,
3657
- stopMindFull,
3658
- matchesGlob,
3659
- SleepManager,
3660
- initSleepManager,
3661
- getSleepManager,
3662
- getSleepManagerIfReady,
3663
- subscribe2 as subscribe,
3664
- subscribeAll,
3665
- publish3 as publish,
3666
- recordInbound,
3667
- recordOutbound,
3668
- linkToolResultToTurn,
3669
- tagUntaggedOutbound,
3670
- tagUntaggedInbound,
3671
- tagRecentInbound,
3672
- resolveSleepAction,
3673
- deliverMessage,
3674
- resetSystemChannelCache,
3675
- ensureSystemChannel,
3676
- joinSystemChannel,
3677
- joinSystemChannelForMind,
3678
- announceToSystem
3679
- };