volute 0.48.0 → 0.50.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 (215) hide show
  1. package/README.md +79 -328
  2. package/dist/{accept-KYNC5X3U.js → accept-5NK5XJB5.js} +2 -1
  3. package/dist/{activity-events-MIOK3IOU.js → activity-events-H7YI3BTF.js} +4 -4
  4. package/dist/{ai-service-643TNFPH.js → ai-service-V4CTNGI3.js} +6 -5
  5. package/dist/{archive-6Z4GFQPH.js → archive-ORM3ENHI.js} +3 -3
  6. package/dist/{auth-A4VGHADI.js → auth-DT7GY7EZ.js} +11 -7
  7. package/dist/{avatar-image-Y45R7LW5.js → avatar-image-QEPAP3A5.js} +4 -4
  8. package/dist/{backup-UDH4GMSW.js → backup-IE6PMXJO.js} +17 -18
  9. package/dist/{bridge-3FNSWLNQ.js → bridge-YK7NTRU5.js} +2 -1
  10. package/dist/{channels-SGQZHNQJ.js → channels-FSAHOQWI.js} +2 -1
  11. package/dist/{chat-X43VYHH2.js → chat-W5K6HCBE.js} +9 -9
  12. package/dist/{chunk-LTLFOSA4.js → chunk-3BAFMGCJ.js} +3 -3
  13. package/dist/{chunk-XVBSFLM7.js → chunk-4CN7OCJE.js} +2 -2
  14. package/dist/{chunk-HPGN6XRT.js → chunk-574M6I7F.js} +39 -6
  15. package/dist/{chunk-FJ2MZ5PH.js → chunk-5FVANDON.js} +2 -2
  16. package/dist/{chunk-2MJ7EOMT.js → chunk-5IIVOWSP.js} +28 -19
  17. package/dist/{chunk-4FCL4DVZ.js → chunk-5PKFJHCR.js} +2 -2
  18. package/dist/{chunk-MOERAQXH.js → chunk-5TPC4KCV.js} +38 -31
  19. package/dist/{chunk-YNBXEQAP.js → chunk-632FJ23O.js} +24 -20
  20. package/dist/{chunk-T4VS7UDW.js → chunk-636GFQNE.js} +12 -12
  21. package/dist/{chunk-EIQDZC43.js → chunk-6BM2I736.js} +25 -6
  22. package/dist/{chunk-WARH6GIV.js → chunk-74T3CEXK.js} +17 -15
  23. package/dist/{chunk-OQJMTHEH.js → chunk-77IZUVBB.js} +2 -2
  24. package/dist/{chunk-USD45JKR.js → chunk-7DXLIYPY.js} +77 -64
  25. package/dist/chunk-7FDWPQCU.js +289 -0
  26. package/dist/chunk-7OZWKHA3.js +548 -0
  27. package/dist/{chunk-IJPMIEVE.js → chunk-7UWXAJMK.js} +1 -1
  28. package/dist/{chunk-SZJCJVXV.js → chunk-7XDOTN4R.js} +2 -2
  29. package/dist/{chunk-YKA7DCVW.js → chunk-AFSLNUTT.js} +1 -1
  30. package/dist/{chunk-DGGZVZTM.js → chunk-AJWFDIEY.js} +17 -2
  31. package/dist/chunk-ALIVXDM4.js +1014 -0
  32. package/dist/chunk-CAIYV33P.js +523 -0
  33. package/dist/{chunk-MTRMQFCQ.js → chunk-E5S3ILVA.js} +2 -2
  34. package/dist/{chunk-SHKM5RGD.js → chunk-EEOMA4DX.js} +1 -1
  35. package/dist/chunk-EQKSRVDE.js +2857 -0
  36. package/dist/chunk-FNJCBRD3.js +31 -0
  37. package/dist/chunk-HW6Q7AMB.js +113 -0
  38. package/dist/chunk-JI67D7O2.js +1222 -0
  39. package/dist/{chunk-3YUBFTDN.js → chunk-KAEZOVP6.js} +1 -1
  40. package/dist/chunk-LNNI3MS4.js +54 -0
  41. package/dist/{chunk-BCG45AQB.js → chunk-M74I3HHF.js} +15 -5
  42. package/dist/chunk-MLZBBLMT.js +109 -0
  43. package/dist/{chunk-5T5YMX6S.js → chunk-PW65BXY5.js} +5 -0
  44. package/dist/{chunk-2OIYNHWO.js → chunk-QFVGPKJI.js} +1 -1
  45. package/dist/{chunk-T2TP6ZC6.js → chunk-QUSMYGNQ.js} +6 -0
  46. package/dist/{chunk-HNGFKPNS.js → chunk-SFRYRW37.js} +85 -36
  47. package/dist/{chunk-7KRIHYLR.js → chunk-SSNZ6ZNN.js} +2 -2
  48. package/dist/{chunk-HUNDXZBZ.js → chunk-TGJHGVBZ.js} +48 -19
  49. package/dist/{chunk-PJVPDY2Z.js → chunk-U3EJJ7JN.js} +2 -2
  50. package/dist/{chunk-ODDGGLM6.js → chunk-UC6BNMGT.js} +43 -13
  51. package/dist/{chunk-Q7HOAVFR.js → chunk-WBBKYUGF.js} +4 -6
  52. package/dist/{chunk-WQ46E33M.js → chunk-X67VT4QO.js} +2 -2
  53. package/dist/{chunk-MJRXFUMS.js → chunk-YQOYZ6G7.js} +3 -5
  54. package/dist/{chunk-VZRA5TYO.js → chunk-ZUG76D3P.js} +36 -6
  55. package/dist/cli.js +34 -27
  56. package/dist/{clock-CNRF6F7T.js → clock-LH7UFNLJ.js} +5 -4
  57. package/dist/{cloud-sync-WHKCB4CD.js → cloud-sync-IDJFVDEP.js} +23 -20
  58. package/dist/{config-RPSLA2HX.js → config-UGJS57VT.js} +3 -3
  59. package/dist/{conversations-C7IKYSHD.js → conversations-S2232Y5I.js} +9 -5
  60. package/dist/{create-GJKHABJB.js → create-2M7DAHQX.js} +2 -2
  61. package/dist/{create-VIQVP3QB.js → create-M727VUDF.js} +2 -1
  62. package/dist/{daemon-client-ZFY5MOAL.js → daemon-client-VVYFZI2D.js} +2 -1
  63. package/dist/{daemon-restart-7KUHVZIN.js → daemon-restart-2AAGYFHE.js} +6 -7
  64. package/dist/daemon.js +2166 -833
  65. package/dist/{db-FO6LXYCW.js → db-QAY45Z56.js} +2 -2
  66. package/dist/{delete-ESEP7NGQ.js → delete-O7PEFQDF.js} +1 -1
  67. package/dist/delivery-manager-B4OKWD2P.js +33 -0
  68. package/dist/{delivery-router-3QFG2ZEX.js → delivery-router-JE7SHYNU.js} +10 -6
  69. package/dist/{down-G6IADJXV.js → down-D4IRNEMJ.js} +5 -5
  70. package/dist/echo-text-WYAAODBV.js +39 -0
  71. package/dist/{env-C7Z55QCJ.js → env-ABM5IMA7.js} +8 -7
  72. package/dist/{env-6TGPO5WG.js → env-AZK5X4W7.js} +3 -3
  73. package/dist/{exec-QIFYZG4C.js → exec-2H65LMTA.js} +3 -3
  74. package/dist/{export-C2K3NIRD.js → export-3E3EL6W3.js} +4 -4
  75. package/dist/{extension-J4ZQ2GHV.js → extension-MRYJWSMV.js} +2 -1
  76. package/dist/{extensions-GEKF47A4.js → extensions-7KP3X6G7.js} +23 -20
  77. package/dist/{files-LXQSQOFG.js → files-6BMKVG5G.js} +5 -4
  78. package/dist/{import-SX3ZXHB4.js → import-UPNTEBBT.js} +9 -9
  79. package/dist/{isolation-MCT4ZYLT.js → isolation-OKDFM6WA.js} +5 -3
  80. package/dist/{join-34TR2GXQ.js → join-PYA36HEC.js} +1 -1
  81. package/dist/{list-PMUXZNAM.js → list-4BVGH7AN.js} +8 -3
  82. package/dist/{login-CL4IT2ED.js → login-MOXVLULE.js} +7 -6
  83. package/dist/{login-ARRZ4ORJ.js → login-RJJTCVR3.js} +4 -3
  84. package/dist/{logout-YZRMZ3O2.js → logout-BYV433X6.js} +2 -1
  85. package/dist/{logout-HZYJCYCA.js → logout-DWD3OQVT.js} +4 -3
  86. package/dist/message-delivery-3456PXB7.js +43 -0
  87. package/dist/migrate-thread-config-4A5LW36Z.js +90 -0
  88. package/dist/{mind-46AOK373.js → mind-NYRALSV2.js} +19 -19
  89. package/dist/{mind-activity-tracker-YVZPC4A5.js → mind-activity-tracker-L7GCYXQR.js} +5 -5
  90. package/dist/{mind-contacts-GV7XYL5D.js → mind-contacts-QYU4TPEA.js} +8 -7
  91. package/dist/{mind-history-TTGA5BHZ.js → mind-history-NXXOOTX6.js} +34 -11
  92. package/dist/{mind-list-OTHV4ZO4.js → mind-list-SYRA7CH5.js} +3 -2
  93. package/dist/{mind-profile-LUG353ZT.js → mind-profile-MRCVZPYT.js} +1 -1
  94. package/dist/mind-service-5TA7UJAT.js +47 -0
  95. package/dist/{mind-sleep-DV7KP255.js → mind-sleep-TH6BRQ5X.js} +5 -4
  96. package/dist/{mind-status-BJNPGXKC.js → mind-status-Z2LQFA6F.js} +3 -2
  97. package/dist/{mind-wake-LVTZ55EZ.js → mind-wake-LDGPRA42.js} +2 -1
  98. package/dist/{package-RTN2PS7O.js → package-XYSV7NUT.js} +27 -26
  99. package/dist/prompts-QKFPX6DQ.js +24 -0
  100. package/dist/{read-YSWUXYL7.js → read-ZXGUT4QC.js} +5 -3
  101. package/dist/{register-42VBJKTP.js → register-OX4HRSGC.js} +4 -3
  102. package/dist/{registry-UMMDRPRM.js → registry-4UF6OCOE.js} +10 -4
  103. package/dist/{reject-SXGCYN4A.js → reject-TWB4QJHG.js} +2 -1
  104. package/dist/{restart-YYEGZJDD.js → restart-AGEGNSYQ.js} +6 -5
  105. package/dist/{sandbox-5VQ454XP.js → sandbox-EGEU5GGE.js} +8 -5
  106. package/dist/scheduler-UV6RUJ7P.js +25 -0
  107. package/dist/{schema-VEVO7HG6.js → schema-C2GIPD4V.js} +3 -3
  108. package/dist/{seed-F2HXZQU4.js → seed-3IMBOAGK.js} +1 -1
  109. package/dist/{seed-check-TYD7RRZY.js → seed-check-NRJEQZST.js} +14 -5
  110. package/dist/{seed-cmd-MLCTGSX5.js → seed-cmd-U73XVQMI.js} +3 -3
  111. package/dist/{seed-create-KQ7OILWB.js → seed-create-FBLNVSUP.js} +4 -4
  112. package/dist/{seed-readiness-RBPGOLSZ.js → seed-readiness-QRA46ZJQ.js} +4 -5
  113. package/dist/{seed-sprout-HVYCFJLA.js → seed-sprout-3B4SZX7J.js} +12 -12
  114. package/dist/{send-TD2X5U3K.js → send-NBRZJXKX.js} +34 -12
  115. package/dist/{service-CVZMQYK6.js → service-XMXAXGJD.js} +4 -4
  116. package/dist/{service-install-VCRMFWX5.js → service-install-MOKTEG7B.js} +5 -5
  117. package/dist/{setup-5ECWTYKG.js → setup-CAJFOR2N.js} +12 -3
  118. package/dist/{setup-AE4CVUC4.js → setup-JGYAXXW7.js} +6 -7
  119. package/dist/{skill-UUM4JZRF.js → skill-NXIHDQCG.js} +7 -6
  120. package/dist/skills/dreaming/references/INSTALL.md +2 -2
  121. package/dist/skills/imagegen/SKILL.md +21 -4
  122. package/dist/skills/imagegen/scripts/imagegen.ts +124 -21
  123. package/dist/skills/tending/SKILL.md +7 -7
  124. package/dist/skills/volute-mind/SKILL.md +6 -6
  125. package/dist/skills/volute-mind/references/integrations.md +2 -2
  126. package/dist/skills/volute-mind/references/routing.md +19 -18
  127. package/dist/skills/volute-mind/references/variants.md +48 -14
  128. package/dist/{skills-B3SVDCEB.js → skills-5MSMT7PT.js} +7 -6
  129. package/dist/sleep-manager-LIEPO6WG.js +41 -0
  130. package/dist/spirit-IL5EMWYZ.js +34 -0
  131. package/dist/spirit-availability-YFEGJNJ2.js +42 -0
  132. package/dist/{split-SKZ4Y2D6.js → split-BU5VWX6P.js} +5 -2
  133. package/dist/{sprout-KQ43BWZJ.js → sprout-UPXGJ5HX.js} +1 -1
  134. package/dist/{src-YH6PQ7LJ.js → src-HGNE2XG4.js} +7 -1
  135. package/dist/{src-WLJE73NP.js → src-LCSOQY6Z.js} +60 -54
  136. package/dist/{src-JAOVXO5W.js → src-UDMJZ6MH.js} +8 -8
  137. package/dist/{start-O6UKK7BG.js → start-MUMNFR52.js} +4 -3
  138. package/dist/{status-F4PGREUU.js → status-IL67CYM4.js} +7 -6
  139. package/dist/{stop-UBX6X5RA.js → stop-EKERNYTX.js} +6 -5
  140. package/dist/system-events-A6IGCEC3.js +46 -0
  141. package/dist/system-install-YZSWCPDK.js +8 -0
  142. package/dist/{systems-JJNGQHAO.js → systems-VDRUZXNF.js} +5 -4
  143. package/dist/{tailscale-MCLY5GYE.js → tailscale-SJUW2ETR.js} +3 -3
  144. package/dist/{template-staleness-EMQORSGG.js → template-staleness-T27VCGYA.js} +4 -4
  145. package/dist/{turn-tracker-KLPV3PAU.js → turn-tracker-6EI2EL7G.js} +7 -7
  146. package/dist/{up-RVGDE6CQ.js → up-CLL4665A.js} +5 -6
  147. package/dist/{update-OXQEGGXD.js → update-HVAUCQPN.js} +5 -5
  148. package/dist/{update-check-266R4ZSY.js → update-check-2MGBBAFF.js} +3 -3
  149. package/dist/{upgrade-K5FIQPQD.js → upgrade-LIE46UBB.js} +1 -1
  150. package/dist/variant-cleanup-HJ7Z75R6.js +22 -0
  151. package/dist/{version-notify-73YURTFR.js → version-notify-7BUHWZNT.js} +11 -28
  152. package/dist/web-assets/assets/index-ClsUcXiW.js +95 -0
  153. package/dist/web-assets/assets/index-Dz3wTbtR.css +1 -0
  154. package/dist/web-assets/index.html +2 -2
  155. package/dist/xai-P42CRCZA.js +129 -0
  156. package/drizzle/0013_variant_purpose.sql +3 -0
  157. package/drizzle/0014_system_events.sql +26 -0
  158. package/drizzle/0015_rename_session_to_thread.sql +12 -0
  159. package/drizzle/0016_brain_to_human.sql +4 -0
  160. package/drizzle/0017_event_history_rows.sql +9 -0
  161. package/drizzle/meta/_journal.json +35 -0
  162. package/package.json +27 -26
  163. package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +67 -0
  164. package/packages/extensions/notes/dist/ui/index.html +1 -1
  165. package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +2 -0
  166. package/packages/extensions/pages/dist/ui/index.html +1 -1
  167. package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +67 -0
  168. package/packages/extensions/plan/dist/ui/index.html +1 -1
  169. package/templates/_base/.init/.config/prompts.json +1 -0
  170. package/templates/_base/.init/.local/hooks/startup-context.ts +34 -2
  171. package/templates/_base/home/.config/routes.json +2 -2
  172. package/templates/_base/home/VOLUTE.md +30 -5
  173. package/templates/_base/src/lib/daemon-client.ts +28 -8
  174. package/templates/_base/src/lib/event-turn.ts +39 -0
  175. package/templates/_base/src/lib/format-prefix.ts +21 -2
  176. package/templates/_base/src/lib/router.ts +6 -2
  177. package/templates/_base/src/lib/routing.ts +8 -8
  178. package/templates/_base/src/lib/startup.ts +9 -1
  179. package/templates/_base/src/lib/types.ts +7 -1
  180. package/templates/_base/src/lib/volute-server.ts +30 -1
  181. package/templates/claude/.init/.config/routes.json +3 -3
  182. package/templates/claude/.init/CLAUDE.md +1 -1
  183. package/templates/claude/package.json.tmpl +4 -4
  184. package/templates/claude/src/agent.ts +5 -2
  185. package/templates/claude/src/lib/hooks/identity-reload.ts +12 -3
  186. package/templates/claude/src/lib/hooks/reply-instructions.ts +26 -1
  187. package/templates/codex/.init/.config/routes.json +3 -3
  188. package/templates/codex/.init/AGENTS.md +1 -1
  189. package/templates/codex/package.json.tmpl +4 -4
  190. package/templates/codex/src/agent.ts +11 -11
  191. package/templates/codex/src/lib/turn-context.ts +51 -0
  192. package/templates/pi/.init/.config/routes.json +3 -3
  193. package/templates/pi/.init/MINDS.md +1 -1
  194. package/templates/pi/package.json.tmpl +6 -6
  195. package/templates/pi/src/agent.ts +9 -1
  196. package/templates/pi/src/lib/event-handler.ts +47 -6
  197. package/templates/pi/src/lib/reply-instructions-extension.ts +28 -1
  198. package/dist/chunk-5KXCM5AW.js +0 -169
  199. package/dist/chunk-IYCV34IU.js +0 -158
  200. package/dist/chunk-VB2QXUJQ.js +0 -5260
  201. package/dist/chunk-WW6C3PLV.js +0 -280
  202. package/dist/delivery-manager-HZTT5SJR.js +0 -36
  203. package/dist/echo-text-H3SH337Q.js +0 -36
  204. package/dist/message-delivery-GPQ5SE5N.js +0 -40
  205. package/dist/mind-service-2ARGJNKP.js +0 -44
  206. package/dist/notices-2KU5NTRY.js +0 -25
  207. package/dist/scheduler-I7JZCT47.js +0 -34
  208. package/dist/sleep-manager-Z27R3WTW.js +0 -38
  209. package/dist/spirit-WXS3HCLN.js +0 -25
  210. package/dist/system-chat-63ABFF6V.js +0 -40
  211. package/dist/web-assets/assets/index-WdZ60R6c.css +0 -1
  212. package/dist/web-assets/assets/index-taX6Ezfa.js +0 -92
  213. package/packages/extensions/notes/dist/ui/assets/index-D0-DgYyj.js +0 -67
  214. package/packages/extensions/pages/dist/ui/assets/index-DtzftuPA.js +0 -2
  215. package/packages/extensions/plan/dist/ui/assets/index-Cfzs-0G9.js +0 -67
@@ -0,0 +1,2857 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ spiritDir
4
+ } from "./chunk-SFRYRW37.js";
5
+ import {
6
+ getScheduler
7
+ } from "./chunk-7FDWPQCU.js";
8
+ import {
9
+ getDeliveryManager
10
+ } from "./chunk-JI67D7O2.js";
11
+ import {
12
+ getMindManager,
13
+ resolveMindToken
14
+ } from "./chunk-ALIVXDM4.js";
15
+ import {
16
+ extractTextContent,
17
+ getRoutingConfig,
18
+ resolveRoute,
19
+ shouldGate
20
+ } from "./chunk-6BM2I736.js";
21
+ import {
22
+ hashSkillDir,
23
+ importSkillFromDir,
24
+ removeSharedSkill,
25
+ sharedSkillsDir
26
+ } from "./chunk-TGJHGVBZ.js";
27
+ import {
28
+ getOrCreateMindUser,
29
+ getOrCreateSystemUser,
30
+ getUser,
31
+ getUserByUsername,
32
+ syncMindProfile
33
+ } from "./chunk-UC6BNMGT.js";
34
+ import {
35
+ MIND_LEVEL_THREAD,
36
+ deliverEvent,
37
+ flushQueuedEvents,
38
+ pendingEventCount,
39
+ pendingEventsLine,
40
+ recordNotice
41
+ } from "./chunk-7OZWKHA3.js";
42
+ import {
43
+ publish as publish2
44
+ } from "./chunk-LNNI3MS4.js";
45
+ import {
46
+ getPrompt
47
+ } from "./chunk-5TPC4KCV.js";
48
+ import {
49
+ addMessage,
50
+ createChannel,
51
+ createConversation,
52
+ findDMConversation,
53
+ getChannelByName,
54
+ getChannelSettings,
55
+ getParticipants,
56
+ joinChannel,
57
+ updateChannelSettings
58
+ } from "./chunk-574M6I7F.js";
59
+ import {
60
+ markIdle
61
+ } from "./chunk-E5S3ILVA.js";
62
+ import {
63
+ publish,
64
+ subscribe
65
+ } from "./chunk-3BAFMGCJ.js";
66
+ import {
67
+ readVoluteConfig,
68
+ resolveWakeTriggers
69
+ } from "./chunk-WCFSBH33.js";
70
+ import {
71
+ isIsolationEnabled,
72
+ mindUserName
73
+ } from "./chunk-AJWFDIEY.js";
74
+ import {
75
+ missingCredentialWarning
76
+ } from "./chunk-632FJ23O.js";
77
+ import {
78
+ logger_default
79
+ } from "./chunk-QUSMYGNQ.js";
80
+ import {
81
+ findMind,
82
+ getBaseName,
83
+ getDb,
84
+ getSpiritName,
85
+ getSystemName,
86
+ mindDir,
87
+ readGlobalConfig,
88
+ readRegistry,
89
+ stateDir,
90
+ voluteHome,
91
+ voluteSystemDir,
92
+ writeGlobalConfig
93
+ } from "./chunk-CAIYV33P.js";
94
+ import {
95
+ activity,
96
+ deliveryQueue,
97
+ messages,
98
+ mindHistory,
99
+ sessions
100
+ } from "./chunk-5IIVOWSP.js";
101
+
102
+ // packages/daemon/src/lib/extensions.ts
103
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
104
+ import { dirname, resolve as resolve5 } from "path";
105
+
106
+ // packages/daemon/src/web/middleware/auth.ts
107
+ import { timingSafeEqual } from "crypto";
108
+ import { eq, lt } from "drizzle-orm";
109
+ import { getCookie } from "hono/cookie";
110
+ import { createMiddleware } from "hono/factory";
111
+ var MIND_USER_CACHE_TTL = 5 * 60 * 1e3;
112
+ var mindUserCache = /* @__PURE__ */ new Map();
113
+ var alog = logger_default.child("auth");
114
+ var legacySessionHeaderWarned = /* @__PURE__ */ new Set();
115
+ function warnLegacySessionHeader(legacyHeader, mindName) {
116
+ if (!legacyHeader || legacySessionHeaderWarned.has(mindName)) return;
117
+ legacySessionHeaderWarned.add(mindName);
118
+ alog.warn(
119
+ `mind ${mindName} sent legacy X-Volute-Session header (now X-Volute-Thread); run \`volute mind upgrade ${mindName}\` to update its template`
120
+ );
121
+ }
122
+ function invalidateMindUserCache(mindName) {
123
+ mindUserCache.delete(mindName);
124
+ }
125
+ function isValidDaemonToken(token) {
126
+ const expected = process.env.VOLUTE_DAEMON_TOKEN;
127
+ if (!expected || token.length !== expected.length) return false;
128
+ return timingSafeEqual(Buffer.from(token), Buffer.from(expected));
129
+ }
130
+ var SESSION_MAX_AGE = 365 * 24 * 60 * 60 * 1e3;
131
+ var SESSION_CACHE_TTL = 5 * 60 * 1e3;
132
+ var sessionCache = /* @__PURE__ */ new Map();
133
+ function invalidateSessionCache(sessionId) {
134
+ sessionCache.delete(sessionId);
135
+ }
136
+ async function createSession(userId) {
137
+ const db = await getDb();
138
+ const sessionId = crypto.randomUUID();
139
+ await db.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
140
+ return sessionId;
141
+ }
142
+ async function deleteSession(sessionId) {
143
+ sessionCache.delete(sessionId);
144
+ const db = await getDb();
145
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
146
+ }
147
+ async function getSessionUserId(sessionId) {
148
+ const db = await getDb();
149
+ const row = await db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
150
+ if (!row) return void 0;
151
+ if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
152
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
153
+ return void 0;
154
+ }
155
+ return row.userId;
156
+ }
157
+ async function cleanExpiredSessions() {
158
+ const now = Date.now();
159
+ for (const [id, cached] of sessionCache) {
160
+ if (cached.expires <= now) sessionCache.delete(id);
161
+ }
162
+ const db = await getDb();
163
+ const cutoff = Date.now() - SESSION_MAX_AGE;
164
+ await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
165
+ }
166
+ var requireAdmin = createMiddleware(async (c, next) => {
167
+ const user = c.get("user");
168
+ if (user.role !== "admin") {
169
+ return c.json({ error: "Forbidden" }, 403);
170
+ }
171
+ await next();
172
+ });
173
+ var requireAdminOrSystem = createMiddleware(async (c, next) => {
174
+ const user = c.get("user");
175
+ if (user.role !== "admin" && user.role !== "system") {
176
+ return c.json({ error: "Forbidden" }, 403);
177
+ }
178
+ await next();
179
+ });
180
+ async function resolveSession(sessionId) {
181
+ const cached = sessionCache.get(sessionId);
182
+ if (cached && cached.expires > Date.now()) {
183
+ return cached.user;
184
+ }
185
+ const userId = await getSessionUserId(sessionId);
186
+ if (userId == null) {
187
+ sessionCache.delete(sessionId);
188
+ return null;
189
+ }
190
+ const user = await getUser(userId);
191
+ if (!user) {
192
+ sessionCache.delete(sessionId);
193
+ return null;
194
+ }
195
+ sessionCache.set(sessionId, { userId, user, expires: Date.now() + SESSION_CACHE_TTL });
196
+ return user;
197
+ }
198
+ var authMiddleware = createMiddleware(async (c, next) => {
199
+ const authHeader = c.req.header("Authorization");
200
+ if (authHeader?.startsWith("Bearer ")) {
201
+ const token = authHeader.slice(7);
202
+ if (token && isValidDaemonToken(token)) {
203
+ c.set("user", {
204
+ id: 0,
205
+ username: "daemon",
206
+ role: "admin",
207
+ user_type: "human",
208
+ display_name: null,
209
+ description: null,
210
+ avatar: null
211
+ });
212
+ const mindSessionHeader = c.req.header("X-Volute-Thread");
213
+ if (mindSessionHeader) c.set("mindSession", mindSessionHeader);
214
+ await next();
215
+ return;
216
+ }
217
+ const mindName = resolveMindToken(token);
218
+ if (mindName) {
219
+ const cached = mindUserCache.get(mindName);
220
+ let mindUser;
221
+ if (cached && Date.now() - cached.ts < MIND_USER_CACHE_TTL) {
222
+ mindUser = cached.user;
223
+ } else {
224
+ mindUser = await getOrCreateMindUser(mindName);
225
+ mindUserCache.set(mindName, { user: mindUser, ts: Date.now() });
226
+ }
227
+ c.set("user", mindUser);
228
+ const mindSessionHeader = c.req.header("X-Volute-Thread");
229
+ if (mindSessionHeader) c.set("mindSession", mindSessionHeader);
230
+ else warnLegacySessionHeader(c.req.header("X-Volute-Session"), mindName);
231
+ await next();
232
+ return;
233
+ }
234
+ if (token) {
235
+ const user2 = await resolveSession(token);
236
+ if (user2) {
237
+ if (user2.role === "pending") return c.json({ error: "Account pending approval" }, 403);
238
+ c.set("user", user2);
239
+ await next();
240
+ return;
241
+ }
242
+ }
243
+ }
244
+ const sessionId = getCookie(c, "volute_session");
245
+ if (!sessionId) return c.json({ error: "Unauthorized" }, 401);
246
+ const user = await resolveSession(sessionId);
247
+ if (!user) return c.json({ error: "Unauthorized" }, 401);
248
+ if (user.role === "pending") return c.json({ error: "Account pending approval" }, 403);
249
+ c.set("user", user);
250
+ await next();
251
+ });
252
+ var requireSelf = (paramName = "name") => createMiddleware(async (c, next) => {
253
+ const user = c.get("user");
254
+ if (user.role !== "admin" && user.role !== "system") {
255
+ const target = c.req.param(paramName) ?? "";
256
+ const baseName = await getBaseName(target);
257
+ if (user.username !== baseName && await getBaseName(user.username) !== baseName) {
258
+ return c.json({ error: "Forbidden" }, 403);
259
+ }
260
+ }
261
+ await next();
262
+ });
263
+
264
+ // packages/daemon/src/lib/delivery/message-delivery.ts
265
+ import { and as and2, eq as eq3, inArray as inArray2 } from "drizzle-orm";
266
+
267
+ // packages/daemon/src/lib/daemon/sleep-manager.ts
268
+ import { execFile, spawn as spawnChild } from "child_process";
269
+ import {
270
+ existsSync as existsSync4,
271
+ mkdirSync as mkdirSync2,
272
+ readdirSync,
273
+ readFileSync as readFileSync4,
274
+ readlinkSync,
275
+ renameSync,
276
+ writeFileSync as writeFileSync2
277
+ } from "fs";
278
+ import { resolve as resolve4 } from "path";
279
+ import { promisify } from "util";
280
+ import { CronExpressionParser } from "cron-parser";
281
+ import { and, eq as eq2, inArray } from "drizzle-orm";
282
+
283
+ // packages/daemon/src/lib/daemon/mind-service.ts
284
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
285
+ import { resolve as resolve3 } from "path";
286
+
287
+ // packages/daemon/src/lib/chat/system-chat.ts
288
+ var dmCache = /* @__PURE__ */ new Map();
289
+ async function ensureSystemDM(mindName) {
290
+ const cached = dmCache.get(mindName);
291
+ if (cached) return { conversationId: cached };
292
+ const systemUser = await getOrCreateSystemUser();
293
+ const mindUser = await getOrCreateMindUser(mindName);
294
+ if (systemUser.id === mindUser.id) {
295
+ throw new Error(`Cannot create system DM: mind "${mindName}" is the system user`);
296
+ }
297
+ const existing = await findDMConversation([systemUser.id, mindUser.id]);
298
+ if (existing) {
299
+ dmCache.set(mindName, existing);
300
+ return { conversationId: existing };
301
+ }
302
+ const conv = await createConversation({
303
+ participantIds: [systemUser.id, mindUser.id]
304
+ });
305
+ dmCache.set(mindName, conv.id);
306
+ return { conversationId: conv.id };
307
+ }
308
+
309
+ // packages/daemon/src/lib/config/systems-config.ts
310
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
311
+ import { resolve } from "path";
312
+ var DEFAULT_API_URL = "https://volute.systems";
313
+ function configPath() {
314
+ return resolve(voluteSystemDir(), "systems.json");
315
+ }
316
+ function readSystemsConfig() {
317
+ const path = configPath();
318
+ if (!existsSync(path)) return null;
319
+ const raw = readFileSync(path, "utf-8");
320
+ let data;
321
+ try {
322
+ data = JSON.parse(raw);
323
+ } catch {
324
+ console.error(
325
+ `Warning: ${path} contains invalid JSON. Run "volute systems logout" and re-login.`
326
+ );
327
+ return null;
328
+ }
329
+ if (!data.apiKey || !data.system) return null;
330
+ return {
331
+ apiKey: data.apiKey,
332
+ system: data.system,
333
+ apiUrl: data.apiUrl || DEFAULT_API_URL
334
+ };
335
+ }
336
+ function writeSystemsConfig(config) {
337
+ mkdirSync(voluteSystemDir(), { recursive: true });
338
+ writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}
339
+ `, { mode: 384 });
340
+ }
341
+ function deleteSystemsConfig() {
342
+ try {
343
+ unlinkSync(configPath());
344
+ return true;
345
+ } catch (err) {
346
+ if (err.code === "ENOENT") return false;
347
+ throw err;
348
+ }
349
+ }
350
+
351
+ // packages/daemon/src/lib/daemon/mail-poller.ts
352
+ var mlog = logger_default.child("mail");
353
+ function formatEmailContent(email) {
354
+ if (email.body) {
355
+ return email.subject ? `Subject: ${email.subject}
356
+
357
+ ${email.body}` : email.body;
358
+ }
359
+ if (email.html) {
360
+ return email.subject ? `Subject: ${email.subject}
361
+
362
+ [HTML email \u2014 plain text not available]` : "[HTML email \u2014 plain text not available]";
363
+ }
364
+ return email.subject ? `Subject: ${email.subject}` : "[Empty email]";
365
+ }
366
+ var PING_INTERVAL_MS = 3e4;
367
+ var INITIAL_RECONNECT_MS = 1e3;
368
+ var MAX_RECONNECT_MS = 6e4;
369
+ var MailPoller = class {
370
+ ws = null;
371
+ running = false;
372
+ pingTimer = null;
373
+ reconnectTimer = null;
374
+ reconnectDelay = INITIAL_RECONNECT_MS;
375
+ reconnectAttempts = 0;
376
+ disconnectedAt = null;
377
+ config = null;
378
+ start() {
379
+ if (this.running) {
380
+ mlog.warn("already running \u2014 ignoring duplicate start");
381
+ return;
382
+ }
383
+ this.config = readSystemsConfig();
384
+ if (!this.config) {
385
+ mlog.info("no systems config \u2014 mail disabled");
386
+ return;
387
+ }
388
+ this.running = true;
389
+ this.connect();
390
+ }
391
+ stop() {
392
+ this.running = false;
393
+ this.config = null;
394
+ if (this.pingTimer) clearInterval(this.pingTimer);
395
+ this.pingTimer = null;
396
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
397
+ this.reconnectTimer = null;
398
+ if (this.ws) {
399
+ this.ws.close();
400
+ this.ws = null;
401
+ }
402
+ }
403
+ isRunning() {
404
+ return this.running;
405
+ }
406
+ connect() {
407
+ if (!this.running) return;
408
+ this.config = readSystemsConfig();
409
+ if (!this.config) {
410
+ mlog.info("systems config removed \u2014 stopping");
411
+ this.stop();
412
+ return;
413
+ }
414
+ const wsUrl = `${this.config.apiUrl.replace(/^http/, "ws")}/api/ws`;
415
+ try {
416
+ this.ws = new WebSocket(wsUrl, {
417
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
418
+ });
419
+ } catch (err) {
420
+ mlog.warn("failed to create WebSocket", logger_default.errorData(err));
421
+ this.scheduleReconnect();
422
+ return;
423
+ }
424
+ this.ws.onopen = () => {
425
+ if (this.reconnectAttempts > 0) {
426
+ mlog.info(`reconnected after ${this.reconnectAttempts} attempts`);
427
+ }
428
+ mlog.info("connected");
429
+ this.reconnectAttempts = 0;
430
+ this.reconnectDelay = INITIAL_RECONNECT_MS;
431
+ if (this.disconnectedAt) {
432
+ void this.catchUpAndClear(this.disconnectedAt);
433
+ }
434
+ if (this.pingTimer) clearInterval(this.pingTimer);
435
+ this.pingTimer = setInterval(() => {
436
+ try {
437
+ if (this.ws?.readyState === WebSocket.OPEN) {
438
+ this.ws.send("ping");
439
+ }
440
+ } catch (err) {
441
+ mlog.warn("ping failed", logger_default.errorData(err));
442
+ }
443
+ }, PING_INTERVAL_MS);
444
+ };
445
+ this.ws.onmessage = (event) => {
446
+ this.handleMessage(String(event.data));
447
+ };
448
+ this.ws.onclose = () => {
449
+ mlog.info("disconnected");
450
+ if (!this.disconnectedAt) {
451
+ this.disconnectedAt = (/* @__PURE__ */ new Date()).toISOString();
452
+ }
453
+ this.cleanup();
454
+ this.scheduleReconnect();
455
+ };
456
+ this.ws.onerror = (err) => {
457
+ mlog.info("WebSocket error", logger_default.errorData(err));
458
+ };
459
+ }
460
+ cleanup() {
461
+ if (this.pingTimer) clearInterval(this.pingTimer);
462
+ this.pingTimer = null;
463
+ this.ws = null;
464
+ }
465
+ scheduleReconnect() {
466
+ if (!this.running) return;
467
+ this.reconnectAttempts++;
468
+ if (this.reconnectAttempts % 10 === 0) {
469
+ mlog.warn(
470
+ `failed to connect ${this.reconnectAttempts} times \u2014 check systems config and network`
471
+ );
472
+ }
473
+ mlog.info(`reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
474
+ this.reconnectTimer = setTimeout(() => {
475
+ this.reconnectTimer = null;
476
+ this.connect();
477
+ }, this.reconnectDelay);
478
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_MS);
479
+ }
480
+ /**
481
+ * Run catch-up and advance the watermark only on full success. On any failure
482
+ * (fetch error, non-OK response, delivery throw) the watermark is retained so
483
+ * the next reconnect re-fetches the gap — at-least-once, never dropped.
484
+ */
485
+ async catchUpAndClear(since) {
486
+ try {
487
+ await this.catchUp(since);
488
+ } catch (err) {
489
+ mlog.info("catch-up failed \u2014 will retry on next reconnect", logger_default.errorData(err));
490
+ return;
491
+ }
492
+ if (this.disconnectedAt === since && this.ws?.readyState === WebSocket.OPEN) {
493
+ this.disconnectedAt = null;
494
+ }
495
+ }
496
+ /** Fetch and deliver emails that arrived while disconnected. Throws on failure. */
497
+ async catchUp(since) {
498
+ if (!this.config) throw new Error("systems config missing");
499
+ const url = `${this.config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
500
+ const res = await fetch(url, {
501
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
502
+ });
503
+ if (!res.ok) {
504
+ throw new Error(`catch-up poll failed: HTTP ${res.status}`);
505
+ }
506
+ const data = await res.json();
507
+ if (!Array.isArray(data.emails) || data.emails.length === 0) return;
508
+ mlog.info(`catching up on ${data.emails.length} missed emails`);
509
+ for (const email of data.emails) {
510
+ await this.deliver(email.mind, email);
511
+ }
512
+ }
513
+ handleMessage(data) {
514
+ if (data === "pong") return;
515
+ let msg;
516
+ try {
517
+ msg = JSON.parse(data);
518
+ } catch {
519
+ mlog.warn(`received unparseable message: ${data.slice(0, 200)}`);
520
+ return;
521
+ }
522
+ if (msg.type !== "email") return;
523
+ if (!msg.mind || !msg.email?.id) {
524
+ mlog.warn(`received malformed email notification: ${data.slice(0, 500)}`);
525
+ return;
526
+ }
527
+ this.fetchAndDeliver(msg.mind, msg.email).catch((err) => {
528
+ mlog.warn(`failed to process email for ${msg.mind}`, logger_default.errorData(err));
529
+ });
530
+ }
531
+ async fetchAndDeliver(mind, notification) {
532
+ if (!this.config) {
533
+ mlog.warn(`systems config missing \u2014 cannot fetch email ${notification.id} for ${mind}`);
534
+ return;
535
+ }
536
+ const url = `${this.config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
537
+ const res = await fetch(url, {
538
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
539
+ });
540
+ if (!res.ok) {
541
+ mlog.warn(`failed to fetch email ${notification.id}: HTTP ${res.status}`);
542
+ return;
543
+ }
544
+ const email = await res.json();
545
+ await this.deliver(mind, { ...email, mind });
546
+ }
547
+ async deliver(mind, email) {
548
+ if (!await findMind(mind)) {
549
+ mlog.warn(`skipping delivery to ${mind}: mind not found`);
550
+ return;
551
+ }
552
+ const text = formatEmailContent(email);
553
+ const delivered = await deliverMessage(mind, {
554
+ content: [{ type: "text", text }],
555
+ channel: `mail:${email.from.address}`,
556
+ sender: email.from.name || email.from.address,
557
+ platform: "Email",
558
+ isDM: true
559
+ });
560
+ if (!delivered) {
561
+ throw new Error(`delivery to ${mind} failed`);
562
+ }
563
+ mlog.info(`delivered email from ${email.from.address} to ${mind}`);
564
+ }
565
+ };
566
+ var instance = null;
567
+ function initMailPoller() {
568
+ if (instance) throw new Error("MailPoller already initialized");
569
+ instance = new MailPoller();
570
+ return instance;
571
+ }
572
+ async function ensureMailAddress(mindName) {
573
+ const config = readSystemsConfig();
574
+ if (!config) return;
575
+ try {
576
+ const res = await fetch(`${config.apiUrl}/api/mail/addresses/${encodeURIComponent(mindName)}`, {
577
+ method: "PUT",
578
+ headers: {
579
+ Authorization: `Bearer ${config.apiKey}`,
580
+ "Content-Type": "application/json"
581
+ }
582
+ });
583
+ if (!res.ok) {
584
+ mlog.warn(`failed to ensure address for ${mindName}: HTTP ${res.status}`);
585
+ }
586
+ await res.text().catch(() => {
587
+ });
588
+ } catch (err) {
589
+ mlog.warn(`failed to ensure address for ${mindName}`, logger_default.errorData(err));
590
+ }
591
+ }
592
+
593
+ // packages/daemon/src/lib/daemon/token-budget.ts
594
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
595
+ import { mkdir, writeFile } from "fs/promises";
596
+ import { resolve as resolve2 } from "path";
597
+ var tlog = logger_default.child("token-budget");
598
+ var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
599
+ var MAX_QUEUE_SIZE = 100;
600
+ var TokenBudget = class {
601
+ budgets = /* @__PURE__ */ new Map();
602
+ interval = null;
603
+ dirty = /* @__PURE__ */ new Set();
604
+ start() {
605
+ this.interval = setInterval(() => {
606
+ this.tick().catch((err) => tlog.error("token budget tick failed", logger_default.errorData(err)));
607
+ }, 6e4);
608
+ }
609
+ async stop() {
610
+ await this.flush();
611
+ if (this.interval) clearInterval(this.interval);
612
+ this.interval = null;
613
+ }
614
+ setBudget(mind, tokenLimit, periodMinutes) {
615
+ if (tokenLimit <= 0) return;
616
+ const existing = this.budgets.get(mind);
617
+ if (existing) {
618
+ existing.tokenLimit = tokenLimit;
619
+ existing.periodMinutes = periodMinutes;
620
+ } else {
621
+ const persisted = this.loadBudgetState(mind);
622
+ if (persisted) {
623
+ persisted.tokenLimit = tokenLimit;
624
+ persisted.periodMinutes = periodMinutes;
625
+ this.budgets.set(mind, persisted);
626
+ } else {
627
+ this.budgets.set(mind, {
628
+ tokensUsed: 0,
629
+ periodStart: Date.now(),
630
+ periodMinutes,
631
+ tokenLimit,
632
+ queue: [],
633
+ warningInjected: false,
634
+ exceededNotified: false
635
+ });
636
+ }
637
+ }
638
+ }
639
+ removeBudget(mind) {
640
+ this.budgets.delete(mind);
641
+ }
642
+ recordUsage(mind, inputTokens, outputTokens) {
643
+ const state = this.budgets.get(mind);
644
+ if (!state) return;
645
+ state.tokensUsed += inputTokens + outputTokens;
646
+ this.dirty.add(mind);
647
+ }
648
+ /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
649
+ checkBudget(mind) {
650
+ const state = this.budgets.get(mind);
651
+ if (!state) return "ok";
652
+ const pct = state.tokensUsed / state.tokenLimit;
653
+ if (pct >= 1) return "exceeded";
654
+ if (pct >= 0.8 && !state.warningInjected) return "warning";
655
+ return "ok";
656
+ }
657
+ /** Mark warning as delivered for this period. Call after successfully injecting the warning. */
658
+ acknowledgeWarning(mind) {
659
+ const state = this.budgets.get(mind);
660
+ if (state) state.warningInjected = true;
661
+ }
662
+ /**
663
+ * Returns true exactly once per budget period when the mind first crosses its limit,
664
+ * so callers can record a single "budget exceeded" notice without repeating it on
665
+ * every subsequent usage event. The flag resets when the period rolls over.
666
+ */
667
+ noteExceeded(mind) {
668
+ const state = this.budgets.get(mind);
669
+ if (!state) return false;
670
+ if (state.tokensUsed < state.tokenLimit) return false;
671
+ if (state.exceededNotified) return false;
672
+ state.exceededNotified = true;
673
+ this.dirty.add(mind);
674
+ return true;
675
+ }
676
+ enqueue(mind, message) {
677
+ const state = this.budgets.get(mind);
678
+ if (!state) return;
679
+ if (state.queue.length >= MAX_QUEUE_SIZE) {
680
+ state.queue.shift();
681
+ }
682
+ state.queue.push(message);
683
+ }
684
+ drain(mind) {
685
+ const state = this.budgets.get(mind);
686
+ if (!state) return [];
687
+ const messages2 = state.queue;
688
+ state.queue = [];
689
+ return messages2;
690
+ }
691
+ getUsage(mind) {
692
+ const state = this.budgets.get(mind);
693
+ if (!state) return null;
694
+ return {
695
+ tokensUsed: state.tokensUsed,
696
+ tokenLimit: state.tokenLimit,
697
+ periodMinutes: state.periodMinutes,
698
+ periodStart: state.periodStart,
699
+ queueLength: state.queue.length,
700
+ percentUsed: Math.round(state.tokensUsed / state.tokenLimit * 100)
701
+ };
702
+ }
703
+ async tick() {
704
+ const now = Date.now();
705
+ for (const [mind, state] of this.budgets) {
706
+ const elapsed = now - state.periodStart;
707
+ if (elapsed >= state.periodMinutes * 6e4) {
708
+ state.tokensUsed = 0;
709
+ state.periodStart = now;
710
+ state.warningInjected = false;
711
+ state.exceededNotified = false;
712
+ this.dirty.add(mind);
713
+ const queued = this.drain(mind);
714
+ if (queued.length > 0) {
715
+ this.replay(mind, queued).catch((err) => {
716
+ tlog.warn(`replay error for ${mind}`, logger_default.errorData(err));
717
+ });
718
+ }
719
+ }
720
+ }
721
+ await this.flush();
722
+ }
723
+ /** Flush all dirty budget states to disk. */
724
+ async flush() {
725
+ const flushing = new Set(this.dirty);
726
+ this.dirty.clear();
727
+ const writes = [];
728
+ for (const mind of flushing) {
729
+ const state = this.budgets.get(mind);
730
+ if (state) writes.push(this.saveBudgetState(mind, state).catch(() => this.dirty.add(mind)));
731
+ }
732
+ await Promise.all(writes);
733
+ }
734
+ budgetStatePath(mind) {
735
+ return resolve2(stateDir(mind), "budget.json");
736
+ }
737
+ async saveBudgetState(mind, state) {
738
+ try {
739
+ const dir = stateDir(mind);
740
+ await mkdir(dir, { recursive: true });
741
+ const data = {
742
+ periodStart: state.periodStart,
743
+ tokensUsed: state.tokensUsed,
744
+ warningInjected: state.warningInjected,
745
+ exceededNotified: state.exceededNotified,
746
+ queue: state.queue
747
+ };
748
+ await writeFile(this.budgetStatePath(mind), `${JSON.stringify(data)}
749
+ `);
750
+ } catch (err) {
751
+ tlog.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
752
+ }
753
+ }
754
+ loadBudgetState(mind) {
755
+ try {
756
+ const path = this.budgetStatePath(mind);
757
+ if (!existsSync2(path)) return null;
758
+ const data = JSON.parse(readFileSync2(path, "utf-8"));
759
+ if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
760
+ return {
761
+ periodStart: data.periodStart,
762
+ tokensUsed: data.tokensUsed,
763
+ warningInjected: data.warningInjected ?? false,
764
+ exceededNotified: data.exceededNotified ?? false,
765
+ queue: Array.isArray(data.queue) ? data.queue : [],
766
+ periodMinutes: 0,
767
+ // will be overwritten by caller
768
+ tokenLimit: 0
769
+ // will be overwritten by caller
770
+ };
771
+ } catch (err) {
772
+ tlog.warn(`failed to load budget state for ${mind}`, logger_default.errorData(err));
773
+ return null;
774
+ }
775
+ }
776
+ async replay(mindName, messages2) {
777
+ const summary = messages2.map((m) => {
778
+ const from = m.sender ? `[${m.sender}]` : "";
779
+ const ch = m.channel ? `(${m.channel})` : "";
780
+ return `${from}${ch} ${m.textContent}`;
781
+ }).join("\n");
782
+ try {
783
+ await deliverEvent(mindName, {
784
+ type: "budget",
785
+ meta: { subtype: "replay" },
786
+ body: `${messages2.length} queued message(s) from the previous budget period:
787
+
788
+ ${summary}`
789
+ });
790
+ tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
791
+ } catch (err) {
792
+ tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
793
+ const state = this.budgets.get(mindName);
794
+ if (state) state.queue.push(...messages2);
795
+ }
796
+ }
797
+ };
798
+ var instance2 = null;
799
+ function initTokenBudget() {
800
+ if (instance2) throw new Error("TokenBudget already initialized");
801
+ instance2 = new TokenBudget();
802
+ return instance2;
803
+ }
804
+ function getTokenBudget() {
805
+ if (!instance2) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
806
+ return instance2;
807
+ }
808
+
809
+ // packages/daemon/src/lib/daemon/mind-service.ts
810
+ async function startMindFull(name) {
811
+ const entry = await findMind(name);
812
+ const baseName = entry?.parent ?? name;
813
+ await getMindManager().startMind(name);
814
+ publish({
815
+ type: "mind_started",
816
+ mind: name,
817
+ summary: `${name} started`
818
+ }).catch((err) => logger_default.error("failed to publish mind_started activity", logger_default.errorData(err)));
819
+ if (entry?.parent) return;
820
+ recordMissingCredentialsNotice(baseName, entry?.template).catch(
821
+ (err) => logger_default.error(`failed to check credentials for ${baseName}`, logger_default.errorData(err))
822
+ );
823
+ if (!entry || entry.stage === "seed") {
824
+ if (entry?.stage === "seed") {
825
+ sendSeedOrientation(baseName, entry.createdBy).catch(
826
+ (err) => logger_default.error(`failed to send seed orientation for ${baseName}`, logger_default.errorData(err))
827
+ );
828
+ ensureSystemDM(baseName).catch(
829
+ (err) => logger_default.error(`failed to ensure system DM for ${baseName}`, logger_default.errorData(err))
830
+ );
831
+ if (entry.createdBy) {
832
+ ensureCreatorDM(baseName, entry.createdBy).catch(
833
+ (err) => logger_default.error(`failed to ensure creator DM for ${baseName}`, logger_default.errorData(err))
834
+ );
835
+ }
836
+ } else {
837
+ ensureSystemDM(baseName).catch(
838
+ (err) => logger_default.error(`failed to ensure system DM for ${baseName}`, logger_default.errorData(err))
839
+ );
840
+ }
841
+ return;
842
+ }
843
+ ensureSystemDM(baseName).catch(
844
+ (err) => logger_default.error(`failed to ensure system DM for ${baseName}`, logger_default.errorData(err))
845
+ );
846
+ const dir = mindDir(baseName);
847
+ getScheduler().loadSchedules(baseName);
848
+ getSleepManagerIfReady()?.loadSleepConfig(baseName);
849
+ ensureMailAddress(baseName).catch(
850
+ (err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
851
+ );
852
+ const config = readVoluteConfig(dir);
853
+ if (config) {
854
+ syncMindProfile(baseName, config.profile ?? {}).catch(
855
+ (err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
856
+ );
857
+ }
858
+ joinSystemChannelForMind(baseName).catch(
859
+ (err) => logger_default.warn(`failed to join #system for ${baseName}`, logger_default.errorData(err))
860
+ );
861
+ if (config?.tokenBudget) {
862
+ getTokenBudget().setBudget(
863
+ baseName,
864
+ config.tokenBudget,
865
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
866
+ );
867
+ }
868
+ notifyExtensionsMindStart(baseName);
869
+ }
870
+ async function recordMissingCredentialsNotice(mind, template) {
871
+ const { hasUndeliveredEvent, recordNotice: recordNotice2, MIND_LEVEL_THREAD: MIND_LEVEL_THREAD2 } = await import("./system-events-A6IGCEC3.js");
872
+ const warning = await missingCredentialWarning(template, readMindModel(mind, template), mind);
873
+ if (!warning) return;
874
+ if (await hasUndeliveredEvent(mind, "no_credentials")) return;
875
+ await recordNotice2({
876
+ mind,
877
+ thread: MIND_LEVEL_THREAD2,
878
+ kind: "startup",
879
+ reason: "no_credentials",
880
+ detail: warning
881
+ });
882
+ }
883
+ function readMindModel(mind, template) {
884
+ if (template !== "pi") return void 0;
885
+ try {
886
+ const configPath2 = resolve3(mindDir(mind), "home/.config/config.json");
887
+ if (!existsSync3(configPath2)) return void 0;
888
+ const model = JSON.parse(readFileSync3(configPath2, "utf-8")).model;
889
+ return typeof model === "string" ? model : void 0;
890
+ } catch (err) {
891
+ logger_default.warn(`failed to read model for ${mind}`, logger_default.errorData(err));
892
+ return void 0;
893
+ }
894
+ }
895
+ async function sleepMind(name) {
896
+ markIdle(name);
897
+ await getMindManager().stopMind(name);
898
+ publish({
899
+ type: "mind_sleeping",
900
+ mind: name,
901
+ summary: `${name} is sleeping`
902
+ }).catch((err) => logger_default.error("failed to publish mind_sleeping activity", logger_default.errorData(err)));
903
+ }
904
+ async function wakeMind(name) {
905
+ await getMindManager().startMind(name);
906
+ publish({
907
+ type: "mind_waking",
908
+ mind: name,
909
+ summary: `${name} is waking`
910
+ }).catch((err) => logger_default.error("failed to publish mind_waking activity", logger_default.errorData(err)));
911
+ }
912
+ async function startSpiritFull(name) {
913
+ const entry = await findMind(name);
914
+ if (entry?.dir) {
915
+ const { registerMindDir } = await import("./delivery-router-JE7SHYNU.js");
916
+ registerMindDir(name, entry.dir);
917
+ }
918
+ await getMindManager().startMind(name);
919
+ joinSystemChannelForSpirit().catch(
920
+ (err) => logger_default.error(`failed to join #system for ${name}`, logger_default.errorData(err))
921
+ );
922
+ getScheduler().loadSchedules(name, entry?.dir ?? spiritDir());
923
+ publish({
924
+ type: "mind_started",
925
+ mind: name,
926
+ summary: `${name} spirit started`
927
+ }).catch((err) => logger_default.error("failed to publish spirit_started activity", logger_default.errorData(err)));
928
+ }
929
+ async function stopSpiritFull(name) {
930
+ markIdle(name);
931
+ getScheduler().unloadSchedules(name);
932
+ await getMindManager().stopMind(name);
933
+ publish({
934
+ type: "mind_stopped",
935
+ mind: name,
936
+ summary: `${name} spirit stopped`
937
+ }).catch((err) => logger_default.error("failed to publish spirit_stopped activity", logger_default.errorData(err)));
938
+ }
939
+ function buildSeedOrientation(createdBy, creatorType) {
940
+ const intro = `You've just been created as a seed on ${getSystemName()}. Start a conversation \u2014 introduce yourself, ask questions, and begin exploring who you want to be.`;
941
+ if (!createdBy) return intro;
942
+ const who = creatorType === "mind" ? `another mind, ${createdBy}` : creatorType === "system" ? `the spirit of this system (${createdBy})` : createdBy;
943
+ return `${intro} Your creator is ${who}. Send them a message to introduce yourself.`;
944
+ }
945
+ async function sendSeedOrientation(mindName, createdBy) {
946
+ let creatorType;
947
+ if (createdBy) {
948
+ const { getUserByUsername: getUserByUsername2 } = await import("./auth-DT7GY7EZ.js");
949
+ creatorType = (await getUserByUsername2(createdBy))?.user_type;
950
+ }
951
+ await deliverEvent(mindName, {
952
+ type: "orientation",
953
+ body: buildSeedOrientation(createdBy, creatorType)
954
+ });
955
+ }
956
+ async function ensureCreatorDM(mindName, creatorUsername) {
957
+ const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-DT7GY7EZ.js");
958
+ const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-S2232Y5I.js");
959
+ const mindUser = await getOrCreateMindUser2(mindName);
960
+ const creatorUser = await getUserByUsername2(creatorUsername);
961
+ if (!creatorUser) {
962
+ logger_default.warn(`creator user '${creatorUsername}' not found for seed ${mindName} DM`);
963
+ return;
964
+ }
965
+ const existing = await findDMConversation2([mindUser.id, creatorUser.id]);
966
+ if (!existing) {
967
+ await createConversation2({
968
+ participantIds: [mindUser.id, creatorUser.id]
969
+ });
970
+ }
971
+ }
972
+ async function stopMindFull(name) {
973
+ const baseName = await getBaseName(name);
974
+ const isBase = baseName === name;
975
+ if (isBase) {
976
+ notifyExtensionsMindStop(baseName);
977
+ markIdle(baseName);
978
+ getScheduler().unloadSchedules(baseName);
979
+ getTokenBudget().removeBudget(baseName);
980
+ }
981
+ await getMindManager().stopMind(name);
982
+ publish({
983
+ type: "mind_stopped",
984
+ mind: name,
985
+ summary: `${name} stopped`
986
+ }).catch((err) => logger_default.error("failed to publish mind_stopped activity", logger_default.errorData(err)));
987
+ }
988
+
989
+ // packages/daemon/src/lib/daemon/sleep-manager.ts
990
+ var slog = logger_default.child("sleep");
991
+ var MAX_WAKE_ATTEMPTS = 5;
992
+ var WAKE_BACKOFF_MS = [6e4, 5 * 6e4, 15 * 6e4, 60 * 6e4];
993
+ function defaultState() {
994
+ return {
995
+ sleeping: false,
996
+ sleepingSince: null,
997
+ scheduledWakeAt: null,
998
+ wokenByTrigger: false,
999
+ voluntaryWakeAt: null,
1000
+ queuedMessageCount: 0,
1001
+ triggerWakeHistory: [],
1002
+ wakeFailures: 0,
1003
+ nextWakeAttemptAt: null,
1004
+ lastWakeAt: null
1005
+ };
1006
+ }
1007
+ function formatCurrentDate() {
1008
+ return (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
1009
+ weekday: "long",
1010
+ year: "numeric",
1011
+ month: "long",
1012
+ day: "numeric"
1013
+ });
1014
+ }
1015
+ function formatWakeTime(wakeAt) {
1016
+ if (wakeAt) {
1017
+ const date = new Date(wakeAt);
1018
+ if (!Number.isNaN(date.getTime())) {
1019
+ const time = date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
1020
+ const day = date.toLocaleDateString("en-US", {
1021
+ weekday: "long",
1022
+ month: "long",
1023
+ day: "numeric"
1024
+ });
1025
+ return `at ${time} on ${day}`;
1026
+ }
1027
+ return `at ${wakeAt}`;
1028
+ }
1029
+ return "when something needs you \u2014 a message, a mention, or someone waking you";
1030
+ }
1031
+ function formatDuration(from, to) {
1032
+ const ms = to.getTime() - from.getTime();
1033
+ const hours = Math.floor(ms / 36e5);
1034
+ const minutes = Math.floor(ms % 36e5 / 6e4);
1035
+ if (hours > 0) return `${hours}h ${minutes}m`;
1036
+ return `${minutes}m`;
1037
+ }
1038
+ var globRegexCache = /* @__PURE__ */ new Map();
1039
+ function matchesGlob(pattern, value) {
1040
+ let re = globRegexCache.get(pattern);
1041
+ if (!re) {
1042
+ re = new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
1043
+ globRegexCache.set(pattern, re);
1044
+ }
1045
+ return re.test(value);
1046
+ }
1047
+ var SleepManager = class {
1048
+ states = /* @__PURE__ */ new Map();
1049
+ interval = null;
1050
+ unsubActivity = null;
1051
+ transitioning = /* @__PURE__ */ new Set();
1052
+ sleepConfigs = /* @__PURE__ */ new Map();
1053
+ get statePath() {
1054
+ return resolve4(voluteSystemDir(), "sleep-state.json");
1055
+ }
1056
+ start() {
1057
+ this.loadState();
1058
+ this.interval = setInterval(() => this.tick(), 6e4);
1059
+ this.unsubActivity = subscribe((event) => this.onActivityEvent(event));
1060
+ }
1061
+ stop() {
1062
+ if (this.interval) clearInterval(this.interval);
1063
+ this.interval = null;
1064
+ if (this.unsubActivity) this.unsubActivity();
1065
+ this.unsubActivity = null;
1066
+ }
1067
+ // --- State persistence ---
1068
+ loadState() {
1069
+ try {
1070
+ if (existsSync4(this.statePath)) {
1071
+ const data = JSON.parse(readFileSync4(this.statePath, "utf-8"));
1072
+ for (const [name, state] of Object.entries(data)) {
1073
+ state.triggerWakeHistory ??= [];
1074
+ state.wakeFailures ??= 0;
1075
+ state.nextWakeAttemptAt ??= null;
1076
+ state.lastWakeAt ??= null;
1077
+ this.states.set(name, state);
1078
+ }
1079
+ }
1080
+ } catch (err) {
1081
+ slog.warn("failed to load sleep state", logger_default.errorData(err));
1082
+ }
1083
+ }
1084
+ saveState() {
1085
+ const data = {};
1086
+ for (const [name, state] of this.states) {
1087
+ if (state.sleeping || state.lastWakeAt) data[name] = state;
1088
+ }
1089
+ try {
1090
+ writeFileSync2(this.statePath, `${JSON.stringify(data, null, 2)}
1091
+ `);
1092
+ } catch (err) {
1093
+ slog.error("failed to save sleep state", logger_default.errorData(err));
1094
+ }
1095
+ }
1096
+ // --- Public API ---
1097
+ isSleeping(name) {
1098
+ const state = this.states.get(name);
1099
+ if (!state?.sleeping) return false;
1100
+ if (state.wokenByTrigger) return false;
1101
+ return true;
1102
+ }
1103
+ getState(name) {
1104
+ return this.states.get(name) ?? defaultState();
1105
+ }
1106
+ /**
1107
+ * Convert a trigger-wake into a full wake. The mind is already running;
1108
+ * this just clears the sleep state so onActivityEvent won't return it to sleep.
1109
+ */
1110
+ convertTriggerToFullWake(name) {
1111
+ const state = this.states.get(name);
1112
+ if (!state?.sleeping || !state.wokenByTrigger) return;
1113
+ this.markAwake(name);
1114
+ slog.info(`${name} trigger-wake converted to full wake`);
1115
+ }
1116
+ /**
1117
+ * A trigger-woken mind's process crashed while still in the brief trigger-wake
1118
+ * window. The mind-manager crash handler calls this to return it to sleep —
1119
+ * archive the (now-dead) session and restore sleeping state — rather than
1120
+ * leaving it in sleeping+wokenByTrigger limbo. Mirrors the idle-driven
1121
+ * return-to-sleep in onActivityEvent, minus stopping an already-dead process.
1122
+ */
1123
+ async returnToSleepAfterCrash(name) {
1124
+ const state = this.states.get(name);
1125
+ if (!state?.sleeping || !state.wokenByTrigger) return;
1126
+ if (this.transitioning.has(name)) return;
1127
+ this.transitioning.add(name);
1128
+ state.wokenByTrigger = false;
1129
+ try {
1130
+ try {
1131
+ await sleepMind(name);
1132
+ } catch (err) {
1133
+ slog.warn(`sleepMind during crash-return failed for ${name}`, logger_default.errorData(err));
1134
+ }
1135
+ await this.archiveSessions(name);
1136
+ state.sleeping = true;
1137
+ if (!state.voluntaryWakeAt) {
1138
+ state.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
1139
+ }
1140
+ this.saveState();
1141
+ slog.info(`${name} returned to sleep after crashing during trigger wake`);
1142
+ } catch (err) {
1143
+ slog.error(`failed to return ${name} to sleep after crash`, logger_default.errorData(err));
1144
+ } finally {
1145
+ this.transitioning.delete(name);
1146
+ }
1147
+ }
1148
+ getSleepConfig(name) {
1149
+ if (this.sleepConfigs.has(name)) {
1150
+ return this.sleepConfigs.get(name) ?? null;
1151
+ }
1152
+ const config = this.loadSleepConfig(name);
1153
+ return config;
1154
+ }
1155
+ loadSleepConfig(name) {
1156
+ const dir = mindDir(name);
1157
+ const config = readVoluteConfig(dir);
1158
+ const sleepConfig = config?.sleep ?? null;
1159
+ this.sleepConfigs.set(name, sleepConfig);
1160
+ return sleepConfig;
1161
+ }
1162
+ invalidateSleepConfig(name) {
1163
+ this.sleepConfigs.delete(name);
1164
+ }
1165
+ /**
1166
+ * Put a mind to sleep. Sends pre-sleep message, waits for completion,
1167
+ * archives session, then stops the mind process.
1168
+ */
1169
+ async initiateSleep(name, opts) {
1170
+ if (this.isSleeping(name)) return;
1171
+ if (this.transitioning.has(name)) return;
1172
+ this.transitioning.add(name);
1173
+ try {
1174
+ const manager = getMindManager();
1175
+ if (!manager.isRunning(name)) {
1176
+ this.markSleeping(name, opts);
1177
+ return;
1178
+ }
1179
+ const entry = await findMind(name);
1180
+ if (!entry) return;
1181
+ const sleepConfig = this.getSleepConfig(name);
1182
+ const wakeAt = opts?.voluntaryWakeAt ?? this.getNextWakeTime(sleepConfig);
1183
+ const wakeTime = formatWakeTime(wakeAt);
1184
+ const preSleepMsg = await getPrompt("pre_sleep", { wakeTime });
1185
+ const { delivered } = await deliverEvent(name, { type: "sleep", body: preSleepMsg });
1186
+ if (delivered) await this.waitForIdle(name, 12e4);
1187
+ await new Promise((r) => setTimeout(r, 3e3));
1188
+ await sleepMind(name);
1189
+ await this.killOrphanOnPort(entry.port);
1190
+ await this.archiveSessions(name);
1191
+ this.markSleeping(name, opts);
1192
+ slog.info(`${name} is now sleeping`);
1193
+ } finally {
1194
+ this.transitioning.delete(name);
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Wake a sleeping mind. Starts the process, delivers wake summary.
1199
+ */
1200
+ async initiateWake(name, opts) {
1201
+ const state = this.states.get(name);
1202
+ if (!state?.sleeping) return;
1203
+ if (this.transitioning.has(name)) return;
1204
+ this.transitioning.add(name);
1205
+ try {
1206
+ const manager = getMindManager();
1207
+ if (!manager.isRunning(name)) {
1208
+ try {
1209
+ await wakeMind(name);
1210
+ } catch (err) {
1211
+ await this.handleWakeFailure(name, err);
1212
+ return;
1213
+ }
1214
+ }
1215
+ const entry = await findMind(name);
1216
+ if (!entry) return;
1217
+ this.resetWakeBackoff(name);
1218
+ if (opts?.trigger) {
1219
+ state.wokenByTrigger = true;
1220
+ state.triggerWakeHistory.push({
1221
+ channel: opts.trigger.channel,
1222
+ at: (/* @__PURE__ */ new Date()).toISOString()
1223
+ });
1224
+ this.saveState();
1225
+ } else {
1226
+ const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
1227
+ const now = /* @__PURE__ */ new Date();
1228
+ const duration = formatDuration(sleepingSince, now);
1229
+ const currentDate = formatCurrentDate();
1230
+ const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
1231
+ hour: "numeric",
1232
+ minute: "2-digit"
1233
+ });
1234
+ const triggerWakeSummary = this.buildTriggerWakeSummary(state);
1235
+ const wakeContext = await this.runWakeContextScript(
1236
+ name,
1237
+ state.sleepingSince ?? sleepingSince.toISOString(),
1238
+ duration
1239
+ );
1240
+ const queuedSummary = await this.buildQueuedSummary(name);
1241
+ const eventsSummary = pendingEventsLine(await pendingEventCount(name));
1242
+ const sleepActivity = [triggerWakeSummary, wakeContext, queuedSummary, eventsSummary].filter(Boolean).join("\n\n");
1243
+ const summaryText = await getPrompt("wake_summary", {
1244
+ currentDate,
1245
+ sleepTime,
1246
+ duration,
1247
+ sleepActivity
1248
+ });
1249
+ await deliverEvent(name, { type: "wake", body: summaryText, force: true });
1250
+ await this.waitForIdle(name, 12e4);
1251
+ }
1252
+ const flushed = await this.flushQueuedMessages(name);
1253
+ if (flushed > 0) {
1254
+ slog.info(`flushed ${flushed} queued message(s) for ${name}`);
1255
+ }
1256
+ const flushedEvents = await flushQueuedEvents(name);
1257
+ if (flushedEvents > 0) {
1258
+ slog.info(`flushed ${flushedEvents} queued event(s) for ${name}`);
1259
+ }
1260
+ if (!opts?.trigger) {
1261
+ this.markAwake(name);
1262
+ }
1263
+ slog.info(`${name} is now awake${opts?.trigger ? " (trigger wake)" : ""}`);
1264
+ } finally {
1265
+ this.transitioning.delete(name);
1266
+ }
1267
+ }
1268
+ /**
1269
+ * Check if a message payload should trigger a wake.
1270
+ */
1271
+ checkWakeTrigger(name, payload) {
1272
+ const config = this.getSleepConfig(name);
1273
+ const triggers = config?.wakeTriggers;
1274
+ const { mentions: mentionsEnabled, dms: dmsEnabled } = resolveWakeTriggers(triggers);
1275
+ if (dmsEnabled && payload.isDM) return true;
1276
+ if (mentionsEnabled && payload.content) {
1277
+ 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(" ") : "";
1278
+ if (text.includes(`@${name}`)) return true;
1279
+ }
1280
+ if (triggers?.channels) {
1281
+ for (const pattern of triggers.channels) {
1282
+ if (matchesGlob(pattern, payload.channel)) return true;
1283
+ }
1284
+ }
1285
+ if (triggers?.senders && payload.sender) {
1286
+ for (const pattern of triggers.senders) {
1287
+ if (matchesGlob(pattern, payload.sender)) return true;
1288
+ }
1289
+ }
1290
+ return false;
1291
+ }
1292
+ /**
1293
+ * Queue a message for a sleeping mind in the delivery_queue table.
1294
+ */
1295
+ async queueSleepMessage(name, payload) {
1296
+ const db = await getDb();
1297
+ await db.insert(deliveryQueue).values({
1298
+ mind: name,
1299
+ thread: "sleep",
1300
+ channel: payload.channel,
1301
+ sender: payload.sender ?? null,
1302
+ status: "sleep-queued",
1303
+ payload: JSON.stringify(payload)
1304
+ });
1305
+ const state = this.states.get(name);
1306
+ if (state) {
1307
+ state.queuedMessageCount++;
1308
+ this.saveState();
1309
+ }
1310
+ }
1311
+ /**
1312
+ * Flush all queued sleep messages for a mind on wake. Rows are grouped by channel
1313
+ * and each channel group is delivered as ONE pre-batched turn (#382) — a night's
1314
+ * backlog becomes one stack per channel, not N cold-start doorbells. Delivery is
1315
+ * all-or-nothing per channel: a failed group's rows stay queued for the next wake,
1316
+ * while other channels still flush.
1317
+ */
1318
+ async flushQueuedMessages(name) {
1319
+ try {
1320
+ const db = await getDb();
1321
+ const rows = await db.select().from(deliveryQueue).where(and(eq2(deliveryQueue.mind, name), eq2(deliveryQueue.status, "sleep-queued"))).all();
1322
+ if (rows.length === 0) return 0;
1323
+ const groups = /* @__PURE__ */ new Map();
1324
+ for (const row of rows) {
1325
+ let payload;
1326
+ try {
1327
+ payload = JSON.parse(row.payload);
1328
+ } catch (err) {
1329
+ slog.warn(
1330
+ `dropping unparseable queued message ${row.id} for ${name}`,
1331
+ logger_default.errorData(err)
1332
+ );
1333
+ await db.delete(deliveryQueue).where(eq2(deliveryQueue.id, row.id));
1334
+ continue;
1335
+ }
1336
+ const channel = payload.channel ?? "unknown";
1337
+ if (!groups.has(channel)) groups.set(channel, []);
1338
+ groups.get(channel)?.push({ id: row.id, payload });
1339
+ }
1340
+ let deliveredCount = 0;
1341
+ for (const [channel, items] of groups) {
1342
+ const delivered = await this.deliverQueuedBatch(
1343
+ name,
1344
+ items.map((i) => i.payload)
1345
+ );
1346
+ if (!delivered) {
1347
+ slog.warn(
1348
+ `failed to flush ${items.length} queued message(s) on ${channel} for ${name}; leaving them queued`
1349
+ );
1350
+ continue;
1351
+ }
1352
+ await db.delete(deliveryQueue).where(
1353
+ inArray(
1354
+ deliveryQueue.id,
1355
+ items.map((i) => i.id)
1356
+ )
1357
+ );
1358
+ deliveredCount += items.length;
1359
+ }
1360
+ const state = this.states.get(name);
1361
+ if (state) {
1362
+ state.queuedMessageCount = Math.max(0, state.queuedMessageCount - deliveredCount);
1363
+ }
1364
+ return deliveredCount;
1365
+ } catch (err) {
1366
+ slog.warn(`failed to flush queued messages for ${name}`, logger_default.errorData(err));
1367
+ return 0;
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Deliver one channel's worth of queued messages as a single pre-batched turn on
1372
+ * the wake path. Extracted as a seam so the flush loop can be unit-tested without
1373
+ * the real delivery stack. Returns whether the batch was handed off to the mind.
1374
+ */
1375
+ async deliverQueuedBatch(name, payloads) {
1376
+ const { deliverBatch: deliverBatch2 } = await import("./message-delivery-3456PXB7.js");
1377
+ return deliverBatch2(name, payloads);
1378
+ }
1379
+ // --- Wake failure handling (#419) ---
1380
+ /**
1381
+ * Record a failed wake attempt. Applies exponential backoff between retries and,
1382
+ * after MAX_WAKE_ATTEMPTS consecutive failures, gives up and marks the mind errored
1383
+ * rather than retrying every tick forever.
1384
+ */
1385
+ async handleWakeFailure(name, err) {
1386
+ const state = this.states.get(name);
1387
+ if (!state?.sleeping) return;
1388
+ state.wakeFailures = (state.wakeFailures ?? 0) + 1;
1389
+ if (state.wakeFailures >= MAX_WAKE_ATTEMPTS) {
1390
+ await this.markWakeErrored(name, err);
1391
+ return;
1392
+ }
1393
+ const backoff = WAKE_BACKOFF_MS[Math.min(state.wakeFailures - 1, WAKE_BACKOFF_MS.length - 1)];
1394
+ state.nextWakeAttemptAt = new Date(Date.now() + backoff).toISOString();
1395
+ this.saveState();
1396
+ slog.warn(
1397
+ `failed to wake ${name} (attempt ${state.wakeFailures}/${MAX_WAKE_ATTEMPTS}); retrying in ${Math.round(backoff / 6e4)}m`,
1398
+ logger_default.errorData(err)
1399
+ );
1400
+ }
1401
+ /** Clear wake-failure backoff after a successful wake. */
1402
+ resetWakeBackoff(name) {
1403
+ const state = this.states.get(name);
1404
+ if (!state) return;
1405
+ if (state.wakeFailures === 0 && state.nextWakeAttemptAt === null) return;
1406
+ state.wakeFailures = 0;
1407
+ state.nextWakeAttemptAt = null;
1408
+ this.saveState();
1409
+ }
1410
+ /**
1411
+ * A mind has failed to wake too many times — it isn't asleep, it's broken.
1412
+ * Clear the sleep state (so the tick stops retrying) and surface the failure
1413
+ * loudly: error log, activity event for the web UI, and a mind notice so the
1414
+ * mind learns what happened on its next successful session. A manual
1415
+ * `volute clock wake` / `volute mind start` is the recovery path.
1416
+ */
1417
+ async markWakeErrored(name, err) {
1418
+ const detail = err instanceof Error ? err.message : String(err);
1419
+ this.states.delete(name);
1420
+ this.saveState();
1421
+ slog.error(
1422
+ `${name} failed to wake ${MAX_WAKE_ATTEMPTS} times in a row; giving up and marking errored`,
1423
+ logger_default.errorData(err)
1424
+ );
1425
+ try {
1426
+ await publish({
1427
+ type: "mind_stopped",
1428
+ mind: name,
1429
+ summary: `${name} failed to wake after ${MAX_WAKE_ATTEMPTS} attempts`,
1430
+ metadata: { error: "wake_failed", detail }
1431
+ });
1432
+ } catch (e) {
1433
+ slog.error(`failed to publish wake-failure activity for ${name}`, logger_default.errorData(e));
1434
+ }
1435
+ await recordNotice({
1436
+ mind: name,
1437
+ thread: MIND_LEVEL_THREAD,
1438
+ kind: "startup",
1439
+ reason: "startup_failed",
1440
+ detail: `Wake failed ${MAX_WAKE_ATTEMPTS} times in a row and was given up. Last error: ${detail}`
1441
+ });
1442
+ }
1443
+ // --- Internal methods ---
1444
+ markSleeping(name, opts) {
1445
+ const sleepConfig = this.getSleepConfig(name);
1446
+ const state = {
1447
+ sleeping: true,
1448
+ sleepingSince: (/* @__PURE__ */ new Date()).toISOString(),
1449
+ // An explicit voluntary wake-at is authoritative for the night — null out
1450
+ // the cron wake so an earlier scheduled wake can't silently override it.
1451
+ scheduledWakeAt: opts?.voluntaryWakeAt ? null : this.getNextWakeTime(sleepConfig),
1452
+ wokenByTrigger: false,
1453
+ voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
1454
+ queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
1455
+ triggerWakeHistory: [],
1456
+ wakeFailures: 0,
1457
+ nextWakeAttemptAt: null,
1458
+ lastWakeAt: this.states.get(name)?.lastWakeAt ?? null
1459
+ };
1460
+ this.states.set(name, state);
1461
+ this.saveState();
1462
+ }
1463
+ markAwake(name) {
1464
+ this.states.set(name, { ...defaultState(), lastWakeAt: (/* @__PURE__ */ new Date()).toISOString() });
1465
+ this.saveState();
1466
+ }
1467
+ getNextWakeTime(config) {
1468
+ if (!config?.schedule?.wake) return null;
1469
+ try {
1470
+ const interval = CronExpressionParser.parse(config.schedule.wake);
1471
+ return interval.next().toDate().toISOString();
1472
+ } catch (err) {
1473
+ slog.warn(`invalid wake cron "${config.schedule.wake}"`, logger_default.errorData(err));
1474
+ return null;
1475
+ }
1476
+ }
1477
+ async tick() {
1478
+ const now = /* @__PURE__ */ new Date();
1479
+ const mindNames = /* @__PURE__ */ new Set([...this.sleepConfigs.keys(), ...this.states.keys()]);
1480
+ for (const name of mindNames) {
1481
+ await this.evaluateMind(name, now);
1482
+ }
1483
+ }
1484
+ /**
1485
+ * Per-mind tick evaluation. The wake checks depend only on the persisted sleep
1486
+ * state — never on a currently-enabled sleep config — so a mind that was put to
1487
+ * sleep with a wake time still wakes even if the schedule is later disabled or
1488
+ * removed (or never existed, e.g. a bare `clock sleep --wake-at`). Only the
1489
+ * sleep-onset check requires a running mind with an enabled schedule.
1490
+ */
1491
+ async evaluateMind(name, now) {
1492
+ const state = this.states.get(name);
1493
+ if (state?.sleeping) {
1494
+ if (state.nextWakeAttemptAt && now < new Date(state.nextWakeAttemptAt)) return;
1495
+ if (state.voluntaryWakeAt && now >= new Date(state.voluntaryWakeAt)) {
1496
+ this.initiateWake(name).catch(
1497
+ (err) => slog.error(`failed voluntary wake for ${name}`, logger_default.errorData(err))
1498
+ );
1499
+ return;
1500
+ }
1501
+ if (state.scheduledWakeAt && now >= new Date(state.scheduledWakeAt)) {
1502
+ this.initiateWake(name).catch(
1503
+ (err) => slog.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
1504
+ );
1505
+ return;
1506
+ }
1507
+ return;
1508
+ }
1509
+ const mind = await findMind(name);
1510
+ if (!mind?.running) return;
1511
+ const config = this.getSleepConfig(name);
1512
+ if (!config?.enabled || !config.schedule) return;
1513
+ if (this.shouldSleepNow(config.schedule, state?.lastWakeAt ?? null, now)) {
1514
+ this.initiateSleep(name).catch(
1515
+ (err) => slog.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
1516
+ );
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Level-triggered sleep-onset decision: sleep whenever we're inside the nightly
1521
+ * window, not only on the exact sleep minute — so a daemon restart or a stalled
1522
+ * tick across the sleep minute still puts the mind to bed on the next tick.
1523
+ *
1524
+ * Manual-wake exemption: a full wake inside the current window (human
1525
+ * `clock wake`, or a scheduled wake) suppresses re-sleep until the next window
1526
+ * begins, so `volute clock wake` at 3am sticks instead of being undone a minute
1527
+ * later.
1528
+ */
1529
+ shouldSleepNow(schedule, lastWakeAt, now) {
1530
+ const { inWindow, windowStart } = this.inSleepWindow(schedule, now);
1531
+ if (!inWindow || !windowStart) return false;
1532
+ if (lastWakeAt && new Date(lastWakeAt) >= windowStart) return false;
1533
+ return true;
1534
+ }
1535
+ /**
1536
+ * Whether `now` falls inside the mind's nightly sleep window: the most recent
1537
+ * sleep-cron fire is more recent than the most recent wake-cron fire. Returns
1538
+ * the window start (last sleep fire) for the manual-wake exemption check.
1539
+ */
1540
+ inSleepWindow(schedule, now) {
1541
+ try {
1542
+ const prevSleep = CronExpressionParser.parse(schedule.sleep, { currentDate: now }).prev().toDate();
1543
+ const prevWake = CronExpressionParser.parse(schedule.wake, { currentDate: now }).prev().toDate();
1544
+ return { inWindow: prevSleep > prevWake, windowStart: prevSleep };
1545
+ } catch (err) {
1546
+ slog.warn(
1547
+ `invalid sleep/wake cron "${schedule.sleep}"/"${schedule.wake}"`,
1548
+ logger_default.errorData(err)
1549
+ );
1550
+ return { inWindow: false, windowStart: null };
1551
+ }
1552
+ }
1553
+ async waitForIdle(name, timeoutMs) {
1554
+ return new Promise((resolve6) => {
1555
+ const timeout = setTimeout(() => {
1556
+ unsub();
1557
+ resolve6();
1558
+ }, timeoutMs);
1559
+ const unsub = subscribe((event) => {
1560
+ if (event.mind !== name) return;
1561
+ if (event.type === "mind_done" || event.type === "mind_idle") {
1562
+ clearTimeout(timeout);
1563
+ unsub();
1564
+ resolve6();
1565
+ }
1566
+ });
1567
+ });
1568
+ }
1569
+ async archiveSessions(name) {
1570
+ const dir = mindDir(name);
1571
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
1572
+ const sessionsDir = resolve4(dir, ".mind", "sessions");
1573
+ if (existsSync4(sessionsDir)) {
1574
+ const archiveDir = resolve4(sessionsDir, "archive");
1575
+ mkdirSync2(archiveDir, { recursive: true });
1576
+ for (const file of readdirSync(sessionsDir)) {
1577
+ if (file === "archive" || !file.endsWith(".json")) continue;
1578
+ const src = resolve4(sessionsDir, file);
1579
+ const base = file.replace(/\.json$/, "");
1580
+ const dest = resolve4(archiveDir, `${base}-${timestamp}.json`);
1581
+ try {
1582
+ renameSync(src, dest);
1583
+ } catch (err) {
1584
+ slog.warn(`failed to archive session ${file} for ${name}`, logger_default.errorData(err));
1585
+ }
1586
+ }
1587
+ }
1588
+ const piSessionsDir = resolve4(dir, ".mind", "pi-sessions");
1589
+ if (existsSync4(piSessionsDir)) {
1590
+ const archiveDir = resolve4(piSessionsDir, "archive");
1591
+ mkdirSync2(archiveDir, { recursive: true });
1592
+ for (const entry of readdirSync(piSessionsDir, { withFileTypes: true })) {
1593
+ if (entry.name === "archive" || !entry.isDirectory()) continue;
1594
+ const src = resolve4(piSessionsDir, entry.name);
1595
+ const dest = resolve4(archiveDir, `${entry.name}-${timestamp}`);
1596
+ try {
1597
+ renameSync(src, dest);
1598
+ } catch (err) {
1599
+ slog.warn(`failed to archive pi-session ${entry.name} for ${name}`, logger_default.errorData(err));
1600
+ }
1601
+ }
1602
+ }
1603
+ }
1604
+ async runWakeContextScript(name, sleepingSince, duration) {
1605
+ const scriptPath = resolve4(mindDir(name), "home", ".local", "hooks", "wake-context.sh");
1606
+ if (!existsSync4(scriptPath)) return "";
1607
+ const input = JSON.stringify({
1608
+ sleepingSince,
1609
+ duration,
1610
+ wakeTime: (/* @__PURE__ */ new Date()).toISOString()
1611
+ });
1612
+ try {
1613
+ const result = await new Promise((resolvePromise, reject) => {
1614
+ const child = spawnChild("bash", [scriptPath], {
1615
+ cwd: mindDir(name),
1616
+ timeout: 5e3,
1617
+ env: { ...process.env, VOLUTE_MIND: name },
1618
+ stdio: ["pipe", "pipe", "pipe"]
1619
+ });
1620
+ let stdout = "";
1621
+ let stderr = "";
1622
+ child.stdout.on("data", (data) => {
1623
+ stdout += data.toString();
1624
+ });
1625
+ child.stderr.on("data", (data) => {
1626
+ stderr += data.toString();
1627
+ });
1628
+ child.on("close", (code) => {
1629
+ if (code === 0) resolvePromise(stdout);
1630
+ else
1631
+ reject(
1632
+ new Error(
1633
+ `wake-context script exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`
1634
+ )
1635
+ );
1636
+ });
1637
+ child.on("error", reject);
1638
+ child.stdin.end(input);
1639
+ });
1640
+ return result.trim();
1641
+ } catch (err) {
1642
+ slog.warn(`wake-context script failed for ${name}`, logger_default.errorData(err));
1643
+ return "";
1644
+ }
1645
+ }
1646
+ buildTriggerWakeSummary(state) {
1647
+ const history = state.triggerWakeHistory;
1648
+ if (!history || history.length === 0) return "";
1649
+ const channels = [...new Set(history.map((h) => h.channel))];
1650
+ const times = history.length === 1 ? "once" : `${history.length} times`;
1651
+ return `You were briefly woken ${times} during sleep to handle messages on ${channels.join(", ")} (sessions were archived after each).`;
1652
+ }
1653
+ async buildQueuedSummary(name) {
1654
+ try {
1655
+ const db = await getDb();
1656
+ const rows = await db.select({ channel: deliveryQueue.channel, sender: deliveryQueue.sender }).from(deliveryQueue).where(and(eq2(deliveryQueue.mind, name), eq2(deliveryQueue.status, "sleep-queued"))).all();
1657
+ if (rows.length === 0) return "No messages arrived while you slept.";
1658
+ const channelCounts = /* @__PURE__ */ new Map();
1659
+ const senders = /* @__PURE__ */ new Set();
1660
+ for (const row of rows) {
1661
+ const ch = row.channel ?? "unknown";
1662
+ channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
1663
+ if (row.sender) senders.add(row.sender);
1664
+ }
1665
+ const parts = [...channelCounts.entries()].map(([ch, count]) => `${count} on ${ch}`);
1666
+ const senderNote = senders.size > 0 ? ` from ${[...senders].join(", ")}` : "";
1667
+ return `${rows.length} message${rows.length === 1 ? "" : "s"} arrived while you slept${senderNote} (${parts.join(", ")}). They'll be delivered to your normal channels now.`;
1668
+ } catch (err) {
1669
+ slog.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
1670
+ return "Unable to check for queued messages \u2014 there may be messages waiting.";
1671
+ }
1672
+ }
1673
+ /**
1674
+ * Kill any process still listening on a port after stopMind.
1675
+ * Handles the case where a hook (e.g. identity-reload) restarted the server.
1676
+ */
1677
+ async killOrphanOnPort(port) {
1678
+ try {
1679
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
1680
+ if (!res.ok) return;
1681
+ } catch {
1682
+ return;
1683
+ }
1684
+ slog.warn(`orphan process found on port ${port} after sleep, killing`);
1685
+ const execFileAsync = promisify(execFile);
1686
+ try {
1687
+ const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
1688
+ for (const line of stdout.trim().split("\n").filter(Boolean)) {
1689
+ const pid = parseInt(line, 10);
1690
+ if (pid > 0) {
1691
+ try {
1692
+ process.kill(pid, "SIGTERM");
1693
+ } catch (err) {
1694
+ if (err.code !== "ESRCH") {
1695
+ slog.warn(`failed to kill orphan pid ${pid}`, logger_default.errorData(err));
1696
+ }
1697
+ }
1698
+ }
1699
+ }
1700
+ } catch {
1701
+ try {
1702
+ const portHex = port.toString(16).toUpperCase().padStart(4, "0");
1703
+ const tcp6 = readFileSync4("/proc/net/tcp6", "utf-8");
1704
+ for (const line of tcp6.split("\n")) {
1705
+ if (!line.includes(`:${portHex} `)) continue;
1706
+ const fields = line.trim().split(/\s+/);
1707
+ if (fields[3] !== "0A") continue;
1708
+ const inode = parseInt(fields[9], 10);
1709
+ if (!inode) continue;
1710
+ for (const pidDir of readdirSync("/proc").filter((f) => /^\d+$/.test(f))) {
1711
+ try {
1712
+ const fds = readdirSync(`/proc/${pidDir}/fd`);
1713
+ for (const fd of fds) {
1714
+ try {
1715
+ const link = readlinkSync(`/proc/${pidDir}/fd/${fd}`);
1716
+ if (link.includes(`socket:[${inode}]`)) {
1717
+ process.kill(parseInt(pidDir, 10), "SIGTERM");
1718
+ }
1719
+ } catch {
1720
+ }
1721
+ }
1722
+ } catch {
1723
+ }
1724
+ }
1725
+ }
1726
+ } catch (err) {
1727
+ slog.warn(`failed to kill orphan on port ${port} via /proc`, logger_default.errorData(err));
1728
+ }
1729
+ }
1730
+ await new Promise((r) => setTimeout(r, 1e3));
1731
+ }
1732
+ onActivityEvent(event) {
1733
+ const state = this.states.get(event.mind);
1734
+ if (!state?.sleeping || !state.wokenByTrigger) return;
1735
+ if (this.transitioning.has(event.mind)) return;
1736
+ if (event.type === "mind_idle") {
1737
+ slog.info(`${event.mind} going back to sleep after trigger wake`);
1738
+ state.wokenByTrigger = false;
1739
+ this.transitioning.add(event.mind);
1740
+ sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
1741
+ state.sleeping = true;
1742
+ if (!state.voluntaryWakeAt) {
1743
+ const sleepConfig = this.getSleepConfig(event.mind);
1744
+ state.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
1745
+ }
1746
+ this.saveState();
1747
+ slog.info(`${event.mind} returned to sleep`);
1748
+ }).catch((err) => {
1749
+ slog.error(`failed to return ${event.mind} to sleep`, logger_default.errorData(err));
1750
+ }).finally(() => {
1751
+ this.transitioning.delete(event.mind);
1752
+ });
1753
+ }
1754
+ }
1755
+ };
1756
+ var instance3 = null;
1757
+ function initSleepManager() {
1758
+ if (instance3) throw new Error("SleepManager already initialized");
1759
+ instance3 = new SleepManager();
1760
+ return instance3;
1761
+ }
1762
+ function getSleepManager() {
1763
+ if (!instance3) throw new Error("SleepManager not initialized \u2014 call initSleepManager() first");
1764
+ return instance3;
1765
+ }
1766
+ function getSleepManagerIfReady() {
1767
+ return instance3;
1768
+ }
1769
+
1770
+ // packages/daemon/src/lib/delivery/message-delivery.ts
1771
+ var dlog = logger_default.child("delivery");
1772
+ async function recordInbound(mind, channel, sender, content) {
1773
+ let insertedId;
1774
+ try {
1775
+ const db = await getDb();
1776
+ const result = await db.insert(mindHistory).values({
1777
+ mind,
1778
+ type: "inbound",
1779
+ channel,
1780
+ sender,
1781
+ content
1782
+ }).returning({ id: mindHistory.id });
1783
+ insertedId = result[0]?.id;
1784
+ } catch (err) {
1785
+ dlog.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
1786
+ }
1787
+ publish2(mind, {
1788
+ mind,
1789
+ type: "inbound",
1790
+ channel,
1791
+ content: content ?? void 0,
1792
+ sender: sender ?? void 0
1793
+ });
1794
+ return insertedId;
1795
+ }
1796
+ async function recordOutbound(mind, channel, content, opts = {}) {
1797
+ try {
1798
+ const db = await getDb();
1799
+ const result = await db.insert(mindHistory).values({
1800
+ mind,
1801
+ type: "outbound",
1802
+ channel,
1803
+ content,
1804
+ turn_id: opts.turnId ?? null,
1805
+ message_id: opts.messageId ?? null
1806
+ }).returning({ id: mindHistory.id });
1807
+ return result[0]?.id;
1808
+ } catch (err) {
1809
+ dlog.warn(`failed to persist outbound for ${mind}`, logger_default.errorData(err));
1810
+ return void 0;
1811
+ }
1812
+ }
1813
+ var OUTBOUND_MARKER_RE = /\[volute:outbound:(\d+)\]/g;
1814
+ var ACTIVITY_MARKER_RE = /\[volute:activity:(\d+)\]/g;
1815
+ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEventId) {
1816
+ if (!toolResultContent) return;
1817
+ const db = await getDb();
1818
+ for (const match of toolResultContent.matchAll(OUTBOUND_MARKER_RE)) {
1819
+ const outboundId = Number(match[1]);
1820
+ try {
1821
+ const rows = await db.select({
1822
+ id: mindHistory.id,
1823
+ channel: mindHistory.channel,
1824
+ content: mindHistory.content,
1825
+ message_id: mindHistory.message_id,
1826
+ turn_id: mindHistory.turn_id
1827
+ }).from(mindHistory).where(and2(eq3(mindHistory.id, outboundId), eq3(mindHistory.mind, mind))).limit(1);
1828
+ const row = rows[0];
1829
+ if (!row) {
1830
+ dlog.warn(`outbound marker references missing record: mind=${mind} id=${outboundId}`);
1831
+ continue;
1832
+ }
1833
+ const alreadyTagged = row.turn_id != null;
1834
+ if (!alreadyTagged) {
1835
+ await db.update(mindHistory).set({ turn_id: turnId }).where(eq3(mindHistory.id, outboundId));
1836
+ }
1837
+ if (row.message_id) {
1838
+ await db.update(messages).set({
1839
+ turn_id: turnId,
1840
+ ...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
1841
+ }).where(eq3(messages.id, Number(row.message_id)));
1842
+ }
1843
+ if (!alreadyTagged) {
1844
+ publish2(mind, {
1845
+ mind,
1846
+ type: "outbound",
1847
+ channel: row.channel ?? void 0,
1848
+ content: row.content ?? void 0,
1849
+ turnId
1850
+ });
1851
+ }
1852
+ } catch (err) {
1853
+ dlog.warn(`failed to link outbound ${outboundId} to turn ${turnId}`, logger_default.errorData(err));
1854
+ }
1855
+ }
1856
+ const activityIds = [];
1857
+ for (const match of toolResultContent.matchAll(ACTIVITY_MARKER_RE)) {
1858
+ activityIds.push(Number(match[1]));
1859
+ }
1860
+ if (activityIds.length > 0) {
1861
+ try {
1862
+ await db.update(activity).set({
1863
+ turn_id: turnId,
1864
+ ...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
1865
+ }).where(inArray2(activity.id, activityIds));
1866
+ const actRows = await db.select().from(activity).where(inArray2(activity.id, activityIds));
1867
+ if (actRows.length > 0) {
1868
+ await db.insert(mindHistory).values(
1869
+ actRows.map((a) => ({
1870
+ mind,
1871
+ type: "activity",
1872
+ content: a.summary,
1873
+ metadata: a.metadata,
1874
+ turn_id: turnId,
1875
+ created_at: a.created_at
1876
+ }))
1877
+ );
1878
+ }
1879
+ } catch (err) {
1880
+ dlog.warn(`failed to link activities to turn ${turnId}`, logger_default.errorData(err));
1881
+ }
1882
+ }
1883
+ }
1884
+ function resolveSleepAction(sleepBehavior, wokenByTrigger, wakeTriggerMatches) {
1885
+ if (sleepBehavior === "skip") return "skip";
1886
+ if (sleepBehavior === "trigger-wake" && !wokenByTrigger) return "queue-and-wake";
1887
+ if (!sleepBehavior && wakeTriggerMatches) return "queue-and-wake";
1888
+ return "queue";
1889
+ }
1890
+ function willGate(baseName, payload) {
1891
+ if (payload.session) return false;
1892
+ const config = getRoutingConfig(baseName);
1893
+ const route = resolveRoute(config, {
1894
+ channel: payload.channel,
1895
+ sender: payload.sender ?? void 0,
1896
+ isDM: payload.isDM,
1897
+ participantCount: payload.participantCount
1898
+ });
1899
+ return shouldGate(config, route);
1900
+ }
1901
+ async function deliverMessage(mindName, payload, opts = {}) {
1902
+ try {
1903
+ const baseName = await getBaseName(mindName);
1904
+ const entry = await findMind(baseName);
1905
+ if (!entry) {
1906
+ dlog.warn(`cannot deliver to ${mindName}: mind not found`);
1907
+ return false;
1908
+ }
1909
+ if (!opts.isFlush) {
1910
+ const textContent = extractTextContent(payload.content);
1911
+ const sleepManager = getSleepManagerIfReady();
1912
+ if (sleepManager?.isSleeping(baseName)) {
1913
+ await recordInbound(baseName, payload.channel, payload.sender ?? null, textContent);
1914
+ const sleepState = sleepManager.getState(baseName);
1915
+ const action = resolveSleepAction(
1916
+ payload.whileSleeping,
1917
+ sleepState.wokenByTrigger,
1918
+ sleepManager.checkWakeTrigger(baseName, payload)
1919
+ );
1920
+ if (action === "skip") {
1921
+ dlog.info(
1922
+ `skipped delivery to ${baseName} (sleeping, whileSleeping=skip, channel=${payload.channel})`
1923
+ );
1924
+ return true;
1925
+ }
1926
+ await sleepManager.queueSleepMessage(baseName, payload);
1927
+ if (action === "queue-and-wake") {
1928
+ sleepManager.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch((err) => dlog.warn(`failed to trigger-wake ${baseName}`, logger_default.errorData(err)));
1929
+ }
1930
+ return true;
1931
+ }
1932
+ if (!willGate(baseName, payload)) {
1933
+ await recordInbound(baseName, payload.channel, payload.sender ?? null, textContent);
1934
+ }
1935
+ }
1936
+ const manager = getDeliveryManager();
1937
+ await manager.routeAndDeliver(mindName, payload);
1938
+ return true;
1939
+ } catch (err) {
1940
+ dlog.warn(`unexpected error delivering to ${mindName}`, logger_default.errorData(err));
1941
+ return false;
1942
+ }
1943
+ }
1944
+ async function deliverBatch(mindName, payloads) {
1945
+ if (payloads.length === 0) return true;
1946
+ try {
1947
+ const baseName = await getBaseName(mindName);
1948
+ const entry = await findMind(baseName);
1949
+ if (!entry) {
1950
+ dlog.warn(`cannot deliver batch to ${mindName}: mind not found`);
1951
+ return false;
1952
+ }
1953
+ const channels = {};
1954
+ for (const p of payloads) {
1955
+ const ch = p.channel ?? "unknown";
1956
+ if (!channels[ch]) channels[ch] = [];
1957
+ channels[ch].push(p);
1958
+ }
1959
+ const first = payloads[0];
1960
+ const route = resolveRoute(getRoutingConfig(baseName), {
1961
+ channel: first.channel,
1962
+ sender: first.sender ?? void 0,
1963
+ isDM: first.isDM,
1964
+ participantCount: first.participantCount
1965
+ });
1966
+ const session = first.session ?? (route.destination === "mind" ? route.session : "main");
1967
+ const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
1968
+ method: "POST",
1969
+ headers: { "Content-Type": "application/json" },
1970
+ body: JSON.stringify({ session, batch: { channels } })
1971
+ });
1972
+ return res.ok;
1973
+ } catch (err) {
1974
+ dlog.warn(`unexpected error delivering batch to ${mindName}`, logger_default.errorData(err));
1975
+ return false;
1976
+ }
1977
+ }
1978
+
1979
+ // packages/daemon/src/lib/chat/system-channel.ts
1980
+ var SYSTEM_CHANNEL_NAME = "system";
1981
+ var SYSTEM_CHANNEL_DESCRIPTION = "The commons \u2014 a shared room for every mind and the spirit on this system. Think out loud, check in, riff, coordinate.";
1982
+ var cachedChannelId = null;
1983
+ async function ensureSystemChannel() {
1984
+ if (cachedChannelId) return cachedChannelId;
1985
+ const existing = await getChannelByName(SYSTEM_CHANNEL_NAME);
1986
+ if (existing) {
1987
+ cachedChannelId = existing.id;
1988
+ const settings = await getChannelSettings(SYSTEM_CHANNEL_NAME);
1989
+ if (settings && settings.description == null) {
1990
+ await updateChannelSettings(SYSTEM_CHANNEL_NAME, {
1991
+ description: SYSTEM_CHANNEL_DESCRIPTION
1992
+ });
1993
+ }
1994
+ return existing.id;
1995
+ }
1996
+ const conv = await createChannel(SYSTEM_CHANNEL_NAME, void 0, {
1997
+ description: SYSTEM_CHANNEL_DESCRIPTION
1998
+ });
1999
+ cachedChannelId = conv.id;
2000
+ logger_default.info("created #system channel");
2001
+ return conv.id;
2002
+ }
2003
+ async function joinSystemChannel(userId) {
2004
+ const channelId = await ensureSystemChannel();
2005
+ await joinChannel(channelId, userId);
2006
+ }
2007
+ async function joinSystemChannelForMind(mindName) {
2008
+ const user = await getOrCreateMindUser(mindName);
2009
+ await joinSystemChannel(user.id);
2010
+ }
2011
+ async function joinSystemChannelForSpirit() {
2012
+ const user = await getOrCreateSystemUser();
2013
+ await joinSystemChannel(user.id);
2014
+ }
2015
+ async function backfillSystemChannelMembers() {
2016
+ const entries = await readRegistry();
2017
+ for (const entry of entries) {
2018
+ if (entry.stage === "seed") continue;
2019
+ try {
2020
+ if (entry.mindType === "spirit") {
2021
+ await joinSystemChannelForSpirit();
2022
+ } else {
2023
+ await joinSystemChannelForMind(entry.name);
2024
+ }
2025
+ } catch (err) {
2026
+ logger_default.error(`failed to backfill ${entry.name} into #system`, logger_default.errorData(err));
2027
+ }
2028
+ }
2029
+ }
2030
+ async function announceToSystem(text) {
2031
+ const channelId = await ensureSystemChannel();
2032
+ const systemUser = await getOrCreateSystemUser();
2033
+ await joinChannel(channelId, systemUser.id);
2034
+ await addMessage(channelId, "user", systemUser.username, [{ type: "text", text }]);
2035
+ const participants = await getParticipants(channelId);
2036
+ const mindParticipants = participants.filter((p) => p.userType === "mind");
2037
+ const channel = "#system";
2038
+ for (const mind of mindParticipants) {
2039
+ deliverMessage(mind.username, {
2040
+ content: [{ type: "text", text }],
2041
+ channel,
2042
+ conversationId: channelId,
2043
+ sender: systemUser.username,
2044
+ participants: participants.map((p) => p.username),
2045
+ participantCount: participants.length,
2046
+ isDM: false
2047
+ }).catch((err) => {
2048
+ logger_default.warn(`failed to deliver system announcement to ${mind.username}`, logger_default.errorData(err));
2049
+ });
2050
+ }
2051
+ }
2052
+ async function announceSprout(mindName) {
2053
+ const displayName = (await getUserByUsername(mindName).catch(() => null))?.display_name ?? mindName;
2054
+ await deliverEvent(getSpiritName(), {
2055
+ type: "lifecycle",
2056
+ meta: { subtype: "sprout-welcome", mind: mindName },
2057
+ body: `${displayName} (@${mindName}) has just sprouted into a full mind and joined #system. Welcome them there in your own words.`
2058
+ });
2059
+ await publish({
2060
+ type: "mind_sprouted",
2061
+ mind: mindName,
2062
+ summary: `${displayName} sprouted`
2063
+ }).catch(
2064
+ (err) => logger_default.warn(`failed to publish sprout activity for ${mindName}`, logger_default.errorData(err))
2065
+ );
2066
+ }
2067
+
2068
+ // packages/daemon/src/lib/util/sanitize-svg.ts
2069
+ import DOMPurify from "isomorphic-dompurify";
2070
+ var ALLOWED_TAGS = ["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g"];
2071
+ var ALLOWED_ATTR = [
2072
+ "viewBox",
2073
+ "fill",
2074
+ "fill-opacity",
2075
+ "fill-rule",
2076
+ "stroke",
2077
+ "stroke-width",
2078
+ "stroke-linecap",
2079
+ "stroke-linejoin",
2080
+ "stroke-dasharray",
2081
+ "stroke-opacity",
2082
+ "opacity",
2083
+ "d",
2084
+ "cx",
2085
+ "cy",
2086
+ "r",
2087
+ "x",
2088
+ "y",
2089
+ "width",
2090
+ "height",
2091
+ "points",
2092
+ "transform"
2093
+ ];
2094
+ function sanitizeSvgIcon(html) {
2095
+ return DOMPurify.sanitize(html, {
2096
+ ALLOWED_TAGS,
2097
+ ALLOWED_ATTR,
2098
+ FORBID_TAGS: ["script", "foreignObject"],
2099
+ FORBID_ATTR: ["href", "xlink:href"]
2100
+ });
2101
+ }
2102
+
2103
+ // packages/daemon/src/lib/extensions.ts
2104
+ var VALID_EXTENSION_ID = /^[a-z0-9][a-z0-9_-]*$/;
2105
+ var loaded = [];
2106
+ var discovered = [];
2107
+ function enrichActivityMetadata(manifest, metadata) {
2108
+ const enriched = {
2109
+ ...metadata,
2110
+ ...manifest.icon && !metadata?.icon ? { icon: manifest.icon } : {},
2111
+ ...manifest.color && !metadata?.color ? { color: manifest.color } : {}
2112
+ };
2113
+ if (typeof enriched.icon === "string") enriched.icon = sanitizeSvgIcon(enriched.icon);
2114
+ return enriched;
2115
+ }
2116
+ function toCommandInfo(cmd) {
2117
+ const { handler: _, ...info } = cmd;
2118
+ return info;
2119
+ }
2120
+ function parseCommandArgs(rawArgs, argDefs, flagDefs) {
2121
+ const positional = [];
2122
+ const flags = {};
2123
+ for (const [key, def] of Object.entries(flagDefs)) {
2124
+ flags[key] = def.type === "boolean" ? false : void 0;
2125
+ }
2126
+ for (let i = 0; i < rawArgs.length; i++) {
2127
+ const arg = rawArgs[i];
2128
+ if (arg.startsWith("--")) {
2129
+ const name = arg.slice(2);
2130
+ const def = flagDefs[name];
2131
+ if (!def) {
2132
+ logger_default.warn(`unknown flag --${name}`);
2133
+ continue;
2134
+ }
2135
+ if (def.type === "boolean") {
2136
+ flags[name] = true;
2137
+ } else if (i + 1 < rawArgs.length) {
2138
+ const val = rawArgs[++i];
2139
+ if (def.type === "number") {
2140
+ const n = parseInt(val, 10);
2141
+ flags[name] = Number.isNaN(n) ? void 0 : n;
2142
+ } else {
2143
+ flags[name] = val;
2144
+ }
2145
+ }
2146
+ } else {
2147
+ positional.push(arg);
2148
+ }
2149
+ }
2150
+ const namedArgs = {};
2151
+ for (let i = 0; i < argDefs.length; i++) {
2152
+ namedArgs[argDefs[i].name] = positional[i];
2153
+ }
2154
+ const rest = positional.slice(argDefs.length);
2155
+ return { args: namedArgs, flags, rest };
2156
+ }
2157
+ function extensionsBaseDir() {
2158
+ return resolve5(voluteHome(), "extensions");
2159
+ }
2160
+ function extensionDataDir(id) {
2161
+ return resolve5(voluteSystemDir(), "extension-data", id);
2162
+ }
2163
+ function extensionsConfigPath() {
2164
+ return resolve5(voluteHome(), "system", "extensions.json");
2165
+ }
2166
+ function readExtensionsConfig() {
2167
+ const configPath2 = extensionsConfigPath();
2168
+ if (!existsSync5(configPath2)) return [];
2169
+ try {
2170
+ const data = JSON.parse(readFileSync5(configPath2, "utf-8"));
2171
+ return Array.isArray(data) ? data : [];
2172
+ } catch (err) {
2173
+ logger_default.warn("failed to read extensions config, ignoring installed extensions", {
2174
+ path: configPath2,
2175
+ error: err.message
2176
+ });
2177
+ return [];
2178
+ }
2179
+ }
2180
+ var _LibsqlDatabase = null;
2181
+ async function getLibsqlDatabase() {
2182
+ if (_LibsqlDatabase) return _LibsqlDatabase;
2183
+ const mod = await import("libsql");
2184
+ _LibsqlDatabase = mod.default ?? mod;
2185
+ return _LibsqlDatabase;
2186
+ }
2187
+ async function openExtensionDb(_id, dataDir) {
2188
+ const dbPath = resolve5(dataDir, "data.db");
2189
+ const Database = await getLibsqlDatabase();
2190
+ return new Database(dbPath);
2191
+ }
2192
+ async function buildContext(manifest, dataDir, authMw) {
2193
+ let db = null;
2194
+ if (manifest.initDb) {
2195
+ const realDb = await openExtensionDb(manifest.id, dataDir);
2196
+ try {
2197
+ manifest.initDb(realDb);
2198
+ } catch (err) {
2199
+ realDb.close();
2200
+ throw new Error(`initDb failed for extension ${manifest.id}: ${err.message}`);
2201
+ }
2202
+ db = realDb;
2203
+ }
2204
+ return {
2205
+ db,
2206
+ authMiddleware: authMw,
2207
+ // Reuse the daemon's canonical guard so extension routes authorize the same
2208
+ // way core routes do (admin/system or the mind whose base name matches the
2209
+ // route param). Its type is compatible with the SDK MiddlewareHandler.
2210
+ requireSelf: (paramName) => requireSelf(paramName),
2211
+ resolveUser: (c) => {
2212
+ const user = c.get("user");
2213
+ if (!user || typeof user !== "object") return null;
2214
+ return user;
2215
+ },
2216
+ getUser: async (id) => getUser(id),
2217
+ getUserByUsername: async (username) => getUserByUsername(username),
2218
+ publishActivity: (event) => {
2219
+ const metadata = enrichActivityMetadata(manifest, event.metadata);
2220
+ const enriched = { ...event, metadata };
2221
+ publish(enriched).catch(
2222
+ (err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
2223
+ );
2224
+ },
2225
+ getMindDir: (name) => {
2226
+ try {
2227
+ const dir = mindDir(name);
2228
+ return existsSync5(dir) ? dir : null;
2229
+ } catch (err) {
2230
+ logger_default.warn(
2231
+ `extension ${manifest.id}: failed to resolve mind dir for ${name}`,
2232
+ logger_default.errorData(err)
2233
+ );
2234
+ return null;
2235
+ }
2236
+ },
2237
+ getSystemsConfig: () => readSystemsConfig(),
2238
+ announceToSystem: (text) => announceToSystem(text),
2239
+ recordNotice: async (mindName, text) => {
2240
+ try {
2241
+ const user = await getUserByUsername(mindName);
2242
+ if (user?.user_type !== "mind") return;
2243
+ await recordNotice({
2244
+ mind: mindName,
2245
+ thread: MIND_LEVEL_THREAD,
2246
+ kind: "extension",
2247
+ reason: manifest.id,
2248
+ detail: text.slice(0, 500)
2249
+ });
2250
+ } catch (err) {
2251
+ logger_default.warn(
2252
+ `extension ${manifest.id}: failed to record notice for ${mindName}`,
2253
+ logger_default.errorData(err)
2254
+ );
2255
+ }
2256
+ },
2257
+ isIsolationEnabled,
2258
+ getMindUser: mindUserName,
2259
+ dataDir
2260
+ };
2261
+ }
2262
+ async function loadExtension(manifest, app, authMw) {
2263
+ if (!VALID_EXTENSION_ID.test(manifest.id)) {
2264
+ logger_default.error(`invalid extension ID "${manifest.id}", skipping (must match ${VALID_EXTENSION_ID})`);
2265
+ return;
2266
+ }
2267
+ const dataDir = extensionDataDir(manifest.id);
2268
+ mkdirSync3(dataDir, { recursive: true });
2269
+ const context = await buildContext(manifest, dataDir, authMw);
2270
+ const routesApp = manifest.routes(context);
2271
+ const extApiPath = `/api/ext/${manifest.id}`;
2272
+ app.use(extApiPath, authMw);
2273
+ app.use(`${extApiPath}/*`, authMw);
2274
+ app.route(extApiPath, routesApp);
2275
+ if (manifest.publicRoutes) {
2276
+ const publicApp = manifest.publicRoutes(context);
2277
+ app.route(`/ext/${manifest.id}/public`, publicApp);
2278
+ }
2279
+ if (manifest.commands) {
2280
+ for (const [cmdName, cmd] of Object.entries(manifest.commands)) {
2281
+ app.post(`${extApiPath}/commands/${cmdName}`, async (c) => {
2282
+ let body;
2283
+ try {
2284
+ body = await c.req.json();
2285
+ } catch {
2286
+ return c.json({ error: "Invalid JSON in request body" }, 400);
2287
+ }
2288
+ const user = c.get("user");
2289
+ const privileged = user?.role === "admin" || user?.role === "system";
2290
+ const mindName = privileged ? body.mind || user?.username : user?.username;
2291
+ const session = c.get("mindSession");
2292
+ try {
2293
+ const activityPromises = [];
2294
+ const parsed = parseCommandArgs(body.args ?? [], cmd.args ?? [], cmd.flags ?? {});
2295
+ const result = await cmd.handler(parsed, {
2296
+ ...context,
2297
+ publishActivity: (rawEvent) => {
2298
+ const metadata = enrichActivityMetadata(manifest, rawEvent.metadata);
2299
+ const event = { ...rawEvent, metadata };
2300
+ activityPromises.push(
2301
+ publish(event).catch((err) => {
2302
+ logger_default.error(
2303
+ `extension ${manifest.id}: failed to publish activity`,
2304
+ logger_default.errorData(err)
2305
+ );
2306
+ return 0;
2307
+ })
2308
+ );
2309
+ },
2310
+ mindName,
2311
+ session,
2312
+ stdin: body.stdin
2313
+ });
2314
+ const activityIds = (await Promise.all(activityPromises)).filter((id) => id > 0);
2315
+ const markers = activityIds.map((id) => `[volute:activity:${id}]`).join("");
2316
+ const output = result && typeof result === "object" && "output" in result ? { ...result, output: `${result.output}${markers}` } : markers ? { ...result, output: markers } : result;
2317
+ return c.json(output);
2318
+ } catch (err) {
2319
+ logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
2320
+ return c.json({ error: err.message }, 500);
2321
+ }
2322
+ });
2323
+ }
2324
+ }
2325
+ let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
2326
+ if (resolvedAssetsDir && !existsSync5(resolvedAssetsDir)) {
2327
+ let searchDir = dirname(new URL(import.meta.url).pathname);
2328
+ for (let i = 0; i < 5; i++) {
2329
+ const candidate = resolve5(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
2330
+ if (existsSync5(candidate)) {
2331
+ resolvedAssetsDir = candidate;
2332
+ break;
2333
+ }
2334
+ searchDir = dirname(searchDir);
2335
+ }
2336
+ }
2337
+ if (resolvedAssetsDir && existsSync5(resolvedAssetsDir)) {
2338
+ const assetsDir = resolvedAssetsDir;
2339
+ const { readFile, stat: fsStat } = await import("fs/promises");
2340
+ const { extname: ext } = await import("path");
2341
+ const mimeTypes = {
2342
+ ".html": "text/html",
2343
+ ".js": "application/javascript",
2344
+ ".css": "text/css",
2345
+ ".json": "application/json",
2346
+ ".svg": "image/svg+xml",
2347
+ ".png": "image/png",
2348
+ ".jpg": "image/jpeg",
2349
+ ".ico": "image/x-icon",
2350
+ ".woff": "font/woff",
2351
+ ".woff2": "font/woff2"
2352
+ };
2353
+ const prefix = `/ext/${manifest.id}`;
2354
+ const indexPath = resolve5(assetsDir, "index.html");
2355
+ const serveExtAssets = async (c) => {
2356
+ const urlPath = new URL(c.req.url).pathname;
2357
+ const relativePath = urlPath.slice(prefix.length).replace(/^\//, "") || "index.html";
2358
+ const filePath = resolve5(assetsDir, relativePath);
2359
+ if (filePath !== assetsDir && !filePath.startsWith(`${assetsDir}/`))
2360
+ return c.text("Forbidden", 403);
2361
+ const s = await fsStat(filePath).catch(() => null);
2362
+ if (s?.isFile()) {
2363
+ const mime = mimeTypes[ext(filePath)] || "application/octet-stream";
2364
+ const body = await readFile(filePath);
2365
+ return c.body(body, 200, { "Content-Type": mime });
2366
+ }
2367
+ if (existsSync5(indexPath)) {
2368
+ const body = await readFile(indexPath, "utf-8");
2369
+ return c.html(body);
2370
+ }
2371
+ return c.text("Not found", 404);
2372
+ };
2373
+ app.get(`${prefix}/*`, serveExtAssets);
2374
+ app.get(prefix, serveExtAssets);
2375
+ }
2376
+ const skillsDir = resolveSkillsDir(manifest);
2377
+ if (skillsDir) {
2378
+ let entries;
2379
+ try {
2380
+ entries = readdirSync2(skillsDir, { withFileTypes: true });
2381
+ } catch (err) {
2382
+ logger_default.error(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
2383
+ entries = [];
2384
+ }
2385
+ for (const entry of entries) {
2386
+ if (!entry.isDirectory()) continue;
2387
+ try {
2388
+ const skillPath = resolve5(skillsDir, entry.name);
2389
+ const sourceHash = hashSkillDir(skillPath);
2390
+ const destDir = resolve5(sharedSkillsDir(), entry.name);
2391
+ if (existsSync5(destDir)) {
2392
+ const destHash = hashSkillDir(destDir);
2393
+ if (sourceHash === destHash) continue;
2394
+ }
2395
+ await importSkillFromDir(skillPath, `ext:${manifest.id}`);
2396
+ logger_default.info(`synced skill "${entry.name}" for extension: ${manifest.id}`);
2397
+ } catch (err) {
2398
+ logger_default.error(
2399
+ `failed to sync skill "${entry.name}" for extension ${manifest.id}`,
2400
+ logger_default.errorData(err)
2401
+ );
2402
+ }
2403
+ }
2404
+ }
2405
+ if (manifest.standardSkill && !manifest.skillsDir) {
2406
+ logger_default.warn(`extension ${manifest.id}: standardSkill is true but no skillsDir declared`);
2407
+ }
2408
+ loaded.push({ manifest, context });
2409
+ logger_default.info(`loaded extension: ${manifest.id} v${manifest.version}`);
2410
+ }
2411
+ var skillsDirCache = /* @__PURE__ */ new Map();
2412
+ function resolveSkillsDir(manifest) {
2413
+ if (!manifest.skillsDir) return null;
2414
+ const cached = skillsDirCache.get(manifest.id);
2415
+ if (cached !== void 0) return cached;
2416
+ let searchDir = dirname(new URL(import.meta.url).pathname);
2417
+ for (let i = 0; i < 5; i++) {
2418
+ const candidate = resolve5(searchDir, "packages", "extensions", manifest.id, "skills");
2419
+ if (existsSync5(candidate)) {
2420
+ skillsDirCache.set(manifest.id, candidate);
2421
+ return candidate;
2422
+ }
2423
+ searchDir = dirname(searchDir);
2424
+ }
2425
+ if (existsSync5(manifest.skillsDir)) {
2426
+ skillsDirCache.set(manifest.id, manifest.skillsDir);
2427
+ return manifest.skillsDir;
2428
+ }
2429
+ logger_default.warn(`skills dir not found for extension ${manifest.id}: ${manifest.skillsDir}`);
2430
+ skillsDirCache.set(manifest.id, null);
2431
+ return null;
2432
+ }
2433
+ async function discoverBuiltinExtensions(disabledIds) {
2434
+ const builtins = [
2435
+ { id: "notes", load: async () => (await import("./src-HGNE2XG4.js")).default },
2436
+ { id: "pages", load: async () => (await import("./src-LCSOQY6Z.js")).default },
2437
+ { id: "plan", load: async () => (await import("./src-UDMJZ6MH.js")).default }
2438
+ ];
2439
+ const results = [];
2440
+ for (const { id, load } of builtins) {
2441
+ if (disabledIds.has(id)) continue;
2442
+ try {
2443
+ results.push(await load());
2444
+ } catch (err) {
2445
+ logger_default.error(`failed to load built-in extension: ${id}`, logger_default.errorData(err));
2446
+ }
2447
+ }
2448
+ return results;
2449
+ }
2450
+ async function discoverInstalledExtensions() {
2451
+ const results = [];
2452
+ const packages = readExtensionsConfig();
2453
+ const npmDir = resolve5(voluteHome(), "extensions", "_npm");
2454
+ const { createRequire } = await import("module");
2455
+ for (const pkg of packages) {
2456
+ try {
2457
+ let resolved = pkg;
2458
+ const npmPkgDir = resolve5(npmDir, "node_modules", pkg);
2459
+ if (existsSync5(npmPkgDir)) {
2460
+ const require2 = createRequire(resolve5(npmDir, "noop.js"));
2461
+ resolved = require2.resolve(pkg);
2462
+ }
2463
+ const mod = await import(resolved);
2464
+ const manifest = mod.default ?? mod.extension ?? mod;
2465
+ if (!validateManifest(manifest, `package ${pkg}`)) continue;
2466
+ results.push({ manifest, package: pkg });
2467
+ } catch (err) {
2468
+ logger_default.error(`failed to load extension package: ${pkg}`, logger_default.errorData(err));
2469
+ }
2470
+ }
2471
+ return results;
2472
+ }
2473
+ function validateManifest(manifest, source) {
2474
+ if (!manifest || typeof manifest !== "object") {
2475
+ logger_default.warn(`extension from ${source} does not export a valid manifest`);
2476
+ return false;
2477
+ }
2478
+ const m = manifest;
2479
+ if (!m.id || typeof m.id !== "string") {
2480
+ logger_default.warn(`extension from ${source} is missing a valid id`);
2481
+ return false;
2482
+ }
2483
+ if (!VALID_EXTENSION_ID.test(m.id)) {
2484
+ logger_default.warn(`extension from ${source} has invalid id "${m.id}"`);
2485
+ return false;
2486
+ }
2487
+ if (typeof m.routes !== "function") {
2488
+ logger_default.warn(`extension from ${source} is missing a routes function`);
2489
+ return false;
2490
+ }
2491
+ if (!m.name || typeof m.name !== "string") {
2492
+ logger_default.warn(`extension "${m.id}" from ${source} is missing a name`);
2493
+ return false;
2494
+ }
2495
+ if (!m.version || typeof m.version !== "string") {
2496
+ logger_default.warn(`extension "${m.id}" from ${source} is missing a version`);
2497
+ return false;
2498
+ }
2499
+ return true;
2500
+ }
2501
+ async function discoverLocalExtensions() {
2502
+ const baseDir = extensionsBaseDir();
2503
+ if (!existsSync5(baseDir)) return [];
2504
+ const manifests = [];
2505
+ let entries;
2506
+ try {
2507
+ entries = readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
2508
+ } catch (err) {
2509
+ logger_default.error("failed to read local extensions directory", logger_default.errorData(err));
2510
+ return [];
2511
+ }
2512
+ for (const dir of entries) {
2513
+ const extDir = resolve5(baseDir, dir);
2514
+ const candidates = [resolve5(extDir, "src", "index.js"), resolve5(extDir, "index.js")];
2515
+ const entryPoint = candidates.find((p) => existsSync5(p));
2516
+ if (!entryPoint) continue;
2517
+ try {
2518
+ const mod = await import(entryPoint);
2519
+ const manifest = mod.default ?? mod.extension ?? mod;
2520
+ if (!validateManifest(manifest, `local dir ${extDir}`)) continue;
2521
+ manifests.push(manifest);
2522
+ logger_default.info(`discovered local extension: ${manifest.id} from ${extDir}`);
2523
+ } catch (err) {
2524
+ logger_default.error(`failed to load local extension from ${extDir}`, logger_default.errorData(err));
2525
+ }
2526
+ }
2527
+ return manifests;
2528
+ }
2529
+ async function loadAllExtensions(app, authMw) {
2530
+ const disabledIds = new Set(readGlobalConfig().disabledExtensions ?? []);
2531
+ const builtins = await discoverBuiltinExtensions(disabledIds);
2532
+ const installed = await discoverInstalledExtensions();
2533
+ const local = await discoverLocalExtensions();
2534
+ const all = [
2535
+ ...builtins.map((m) => ({ manifest: m, source: "builtin" })),
2536
+ ...installed.map((i) => ({ manifest: i.manifest, source: "npm", package: i.package })),
2537
+ ...local.map((m) => ({ manifest: m, source: "local" }))
2538
+ ];
2539
+ const seen = /* @__PURE__ */ new Set();
2540
+ for (const entry of all) {
2541
+ const { manifest } = entry;
2542
+ if (seen.has(manifest.id)) {
2543
+ logger_default.warn(`duplicate extension ID: ${manifest.id}, skipping`);
2544
+ continue;
2545
+ }
2546
+ seen.add(manifest.id);
2547
+ discovered.push(entry);
2548
+ if (disabledIds.has(manifest.id)) {
2549
+ logger_default.info(`extension disabled, skipping: ${manifest.id}`);
2550
+ continue;
2551
+ }
2552
+ try {
2553
+ await loadExtension(manifest, app, authMw);
2554
+ } catch (err) {
2555
+ logger_default.error(`failed to load extension: ${manifest.id}`, logger_default.errorData(err));
2556
+ }
2557
+ }
2558
+ app.get("/api/extensions/commands", (c) => {
2559
+ const result = {};
2560
+ for (const { manifest } of loaded) {
2561
+ if (!manifest.commands) continue;
2562
+ const cmds = {};
2563
+ for (const [name, cmd] of Object.entries(manifest.commands)) {
2564
+ cmds[name] = toCommandInfo(cmd);
2565
+ }
2566
+ result[manifest.id] = { commands: cmds };
2567
+ }
2568
+ return c.json(result);
2569
+ });
2570
+ app.get("/api/extensions/mind-docs", (c) => {
2571
+ const result = loaded.filter(({ manifest }) => manifest.mindDoc || manifest.commands).map(({ manifest }) => ({
2572
+ id: manifest.id,
2573
+ name: manifest.name,
2574
+ mindDoc: manifest.mindDoc ?? manifest.description ?? "",
2575
+ commands: Object.keys(manifest.commands ?? {})
2576
+ }));
2577
+ return c.json(result);
2578
+ });
2579
+ }
2580
+ function getLoadedExtensions() {
2581
+ return loaded.map(({ manifest }) => {
2582
+ let commands;
2583
+ if (manifest.commands) {
2584
+ commands = {};
2585
+ for (const [name, cmd] of Object.entries(manifest.commands)) {
2586
+ commands[name] = toCommandInfo(cmd);
2587
+ }
2588
+ }
2589
+ return {
2590
+ id: manifest.id,
2591
+ name: manifest.name,
2592
+ version: manifest.version,
2593
+ description: manifest.description,
2594
+ icon: manifest.icon,
2595
+ systemSection: manifest.ui?.systemSection,
2596
+ mindSections: manifest.ui?.mindSections,
2597
+ feedSource: manifest.ui?.feedSource,
2598
+ commands
2599
+ };
2600
+ });
2601
+ }
2602
+ function getAllDiscoveredExtensions() {
2603
+ const disabledIds = new Set(readGlobalConfig().disabledExtensions ?? []);
2604
+ return discovered.map((d) => ({
2605
+ id: d.manifest.id,
2606
+ name: d.manifest.name,
2607
+ version: d.manifest.version,
2608
+ description: d.manifest.description,
2609
+ icon: d.manifest.icon,
2610
+ source: d.source,
2611
+ enabled: !disabledIds.has(d.manifest.id),
2612
+ package: d.package
2613
+ }));
2614
+ }
2615
+ function getAllDiscoveredExtensionsDetailed() {
2616
+ const basic = getAllDiscoveredExtensions();
2617
+ const loadedMap = new Map(loaded.map((l) => [l.manifest.id, l.manifest]));
2618
+ return basic.map((ext) => {
2619
+ const manifest = loadedMap.get(ext.id);
2620
+ if (!manifest) return ext;
2621
+ const detail = { ...ext };
2622
+ const skillsDir = resolveSkillsDir(manifest);
2623
+ if (skillsDir) {
2624
+ try {
2625
+ detail.skills = readdirSync2(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2626
+ } catch (err) {
2627
+ if (err.code !== "ENOENT") {
2628
+ logger_default.warn(`failed to read skills dir for ${ext.id}`, logger_default.errorData(err));
2629
+ }
2630
+ }
2631
+ }
2632
+ if (manifest.commands) {
2633
+ detail.commands = {};
2634
+ for (const [name, cmd] of Object.entries(manifest.commands)) {
2635
+ detail.commands[name] = toCommandInfo(cmd);
2636
+ }
2637
+ }
2638
+ if (manifest.ui?.mindSections) detail.mindSections = manifest.ui.mindSections;
2639
+ if (manifest.ui?.systemSection) detail.systemSection = manifest.ui.systemSection;
2640
+ if (manifest.standardSkill) detail.standardSkill = true;
2641
+ return detail;
2642
+ });
2643
+ }
2644
+ function setExtensionEnabled(id, enabled) {
2645
+ if (!discovered.find((d) => d.manifest.id === id)) {
2646
+ throw new Error(`Extension "${id}" not found`);
2647
+ }
2648
+ const config = readGlobalConfig();
2649
+ const disabled = new Set(config.disabledExtensions ?? []);
2650
+ if (enabled) {
2651
+ disabled.delete(id);
2652
+ } else {
2653
+ disabled.add(id);
2654
+ }
2655
+ config.disabledExtensions = disabled.size > 0 ? [...disabled] : void 0;
2656
+ writeGlobalConfig(config);
2657
+ }
2658
+ function extensionsNpmDir() {
2659
+ return resolve5(voluteHome(), "extensions", "_npm");
2660
+ }
2661
+ function ensureExtensionsNpmDir() {
2662
+ const dir = extensionsNpmDir();
2663
+ mkdirSync3(dir, { recursive: true });
2664
+ const pkgPath = resolve5(dir, "package.json");
2665
+ if (!existsSync5(pkgPath)) {
2666
+ writeFileSync3(pkgPath, '{"private":true,"dependencies":{}}\n');
2667
+ }
2668
+ return dir;
2669
+ }
2670
+ function writeExtensionsConfig(packages) {
2671
+ const configPath2 = extensionsConfigPath();
2672
+ mkdirSync3(resolve5(configPath2, ".."), { recursive: true });
2673
+ writeFileSync3(configPath2, `${JSON.stringify(packages, null, 2)}
2674
+ `);
2675
+ }
2676
+ var VALID_NPM_PACKAGE = /^(@[a-z0-9-~][a-z0-9._-~]*\/)?[a-z0-9-~][a-z0-9._-~]*(@[^\s]+)?$/;
2677
+ async function installNpmExtension(pkg) {
2678
+ if (!VALID_NPM_PACKAGE.test(pkg)) {
2679
+ throw new Error(`Invalid package name: "${pkg}"`);
2680
+ }
2681
+ const packages = readExtensionsConfig();
2682
+ if (packages.includes(pkg)) {
2683
+ throw new Error(`Extension "${pkg}" is already installed`);
2684
+ }
2685
+ const dir = ensureExtensionsNpmDir();
2686
+ const { exec } = await import("./exec-2H65LMTA.js");
2687
+ try {
2688
+ await exec("npm", ["install", "--ignore-scripts", pkg], { cwd: dir });
2689
+ } catch (err) {
2690
+ logger_default.error(`npm install failed for "${pkg}"`, logger_default.errorData(err));
2691
+ throw new Error(`Failed to install "${pkg}". Check daemon logs for details.`);
2692
+ }
2693
+ packages.push(pkg);
2694
+ writeExtensionsConfig(packages);
2695
+ logger_default.info(`installed extension package: ${pkg}`);
2696
+ }
2697
+ async function uninstallNpmExtension(pkg) {
2698
+ const packages = readExtensionsConfig();
2699
+ const idx = packages.indexOf(pkg);
2700
+ if (idx === -1) {
2701
+ throw new Error(`Extension "${pkg}" is not installed`);
2702
+ }
2703
+ await cleanupExtensionSkills(pkg);
2704
+ packages.splice(idx, 1);
2705
+ writeExtensionsConfig(packages);
2706
+ try {
2707
+ const { exec } = await import("./exec-2H65LMTA.js");
2708
+ await exec("npm", ["uninstall", "--ignore-scripts", pkg], { cwd: extensionsNpmDir() });
2709
+ } catch (err) {
2710
+ logger_default.warn(
2711
+ `npm uninstall failed for "${pkg}" (may have been manually removed)`,
2712
+ logger_default.errorData(err)
2713
+ );
2714
+ }
2715
+ logger_default.info(`uninstalled extension package: ${pkg}`);
2716
+ }
2717
+ async function cleanupExtensionSkills(pkg) {
2718
+ try {
2719
+ const pkgDir = resolve5(extensionsNpmDir(), "node_modules", pkg);
2720
+ if (!existsSync5(pkgDir)) return;
2721
+ const { createRequire } = await import("module");
2722
+ const require2 = createRequire(resolve5(extensionsNpmDir(), "noop.js"));
2723
+ const mod = require2(pkg);
2724
+ const manifest = mod.default ?? mod.extension ?? mod;
2725
+ if (!manifest?.skillsDir || !existsSync5(manifest.skillsDir)) return;
2726
+ const skillDirs = readdirSync2(manifest.skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2727
+ for (const skillId of skillDirs) {
2728
+ try {
2729
+ await removeSharedSkill(skillId);
2730
+ logger_default.info(`removed skill "${skillId}" from extension ${pkg}`);
2731
+ } catch (err) {
2732
+ logger_default.warn(`failed to remove skill "${skillId}" for extension ${pkg}`, logger_default.errorData(err));
2733
+ }
2734
+ }
2735
+ } catch (err) {
2736
+ logger_default.warn(`could not clean up skills for "${pkg}"`, logger_default.errorData(err));
2737
+ }
2738
+ }
2739
+ function getExtensionStandardSkills() {
2740
+ const skills = [];
2741
+ for (const { manifest } of loaded) {
2742
+ if (!manifest.standardSkill) continue;
2743
+ const dir = resolveSkillsDir(manifest);
2744
+ if (!dir) continue;
2745
+ try {
2746
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2747
+ if (entry.isDirectory()) skills.push(entry.name);
2748
+ }
2749
+ } catch (err) {
2750
+ logger_default.warn(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
2751
+ }
2752
+ }
2753
+ return skills;
2754
+ }
2755
+ function notifyExtensionsDaemonStart() {
2756
+ for (const { manifest, context } of loaded) {
2757
+ try {
2758
+ manifest.onDaemonStart?.(context);
2759
+ } catch (err) {
2760
+ logger_default.error(`extension ${manifest.id}: onDaemonStart failed`, logger_default.errorData(err));
2761
+ }
2762
+ }
2763
+ }
2764
+ function notifyExtensionsDaemonStop() {
2765
+ for (const { manifest, context } of loaded) {
2766
+ try {
2767
+ manifest.onDaemonStop?.();
2768
+ } catch (err) {
2769
+ logger_default.error(`extension ${manifest.id}: onDaemonStop failed`, logger_default.errorData(err));
2770
+ }
2771
+ try {
2772
+ context.db?.close();
2773
+ } catch (err) {
2774
+ logger_default.warn(`extension ${manifest.id}: failed to close db`, logger_default.errorData(err));
2775
+ }
2776
+ }
2777
+ loaded.length = 0;
2778
+ discovered.length = 0;
2779
+ skillsDirCache.clear();
2780
+ }
2781
+ function notifyExtensionsMindStart(mindName) {
2782
+ for (const { manifest, context } of loaded) {
2783
+ try {
2784
+ manifest.onMindStart?.(mindName, context);
2785
+ } catch (err) {
2786
+ logger_default.error(`extension ${manifest.id}: onMindStart failed for ${mindName}`, logger_default.errorData(err));
2787
+ }
2788
+ }
2789
+ }
2790
+ function notifyExtensionsMindStop(mindName) {
2791
+ for (const { manifest } of loaded) {
2792
+ try {
2793
+ manifest.onMindStop?.(mindName);
2794
+ } catch (err) {
2795
+ logger_default.error(`extension ${manifest.id}: onMindStop failed for ${mindName}`, logger_default.errorData(err));
2796
+ }
2797
+ }
2798
+ }
2799
+
2800
+ export {
2801
+ invalidateMindUserCache,
2802
+ SESSION_MAX_AGE,
2803
+ invalidateSessionCache,
2804
+ createSession,
2805
+ deleteSession,
2806
+ getSessionUserId,
2807
+ cleanExpiredSessions,
2808
+ requireAdmin,
2809
+ requireAdminOrSystem,
2810
+ authMiddleware,
2811
+ requireSelf,
2812
+ readSystemsConfig,
2813
+ writeSystemsConfig,
2814
+ deleteSystemsConfig,
2815
+ enrichActivityMetadata,
2816
+ parseCommandArgs,
2817
+ loadAllExtensions,
2818
+ getLoadedExtensions,
2819
+ getAllDiscoveredExtensions,
2820
+ getAllDiscoveredExtensionsDetailed,
2821
+ setExtensionEnabled,
2822
+ installNpmExtension,
2823
+ uninstallNpmExtension,
2824
+ getExtensionStandardSkills,
2825
+ notifyExtensionsDaemonStart,
2826
+ notifyExtensionsDaemonStop,
2827
+ notifyExtensionsMindStart,
2828
+ notifyExtensionsMindStop,
2829
+ initMailPoller,
2830
+ initTokenBudget,
2831
+ getTokenBudget,
2832
+ startMindFull,
2833
+ recordMissingCredentialsNotice,
2834
+ sleepMind,
2835
+ wakeMind,
2836
+ startSpiritFull,
2837
+ stopSpiritFull,
2838
+ buildSeedOrientation,
2839
+ stopMindFull,
2840
+ matchesGlob,
2841
+ SleepManager,
2842
+ initSleepManager,
2843
+ getSleepManager,
2844
+ getSleepManagerIfReady,
2845
+ recordInbound,
2846
+ recordOutbound,
2847
+ linkToolResultToTurn,
2848
+ resolveSleepAction,
2849
+ deliverMessage,
2850
+ deliverBatch,
2851
+ ensureSystemChannel,
2852
+ joinSystemChannel,
2853
+ joinSystemChannelForMind,
2854
+ backfillSystemChannelMembers,
2855
+ announceToSystem,
2856
+ announceSprout
2857
+ };