volute 0.46.0 → 0.48.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 (172) hide show
  1. package/README.md +10 -10
  2. package/dist/{accept-N5XULCXK.js → accept-KYNC5X3U.js} +1 -1
  3. package/dist/{activity-events-NF6VWTJQ.js → activity-events-MIOK3IOU.js} +3 -3
  4. package/dist/{ai-service-UODCCE4O.js → ai-service-643TNFPH.js} +8 -4
  5. package/dist/{archive-WS2L2VIQ.js → archive-6Z4GFQPH.js} +3 -3
  6. package/dist/{auth-CCFAU4GQ.js → auth-A4VGHADI.js} +4 -4
  7. package/dist/avatar-image-Y45R7LW5.js +23 -0
  8. package/dist/{backup-TST5C2YN.js → backup-UDH4GMSW.js} +15 -15
  9. package/dist/{bridge-2PO3AI5D.js → bridge-3FNSWLNQ.js} +1 -1
  10. package/dist/channels-SGQZHNQJ.js +89 -0
  11. package/dist/{chat-ZIQQS25U.js → chat-X43VYHH2.js} +12 -8
  12. package/dist/{chunk-ZV7MFISX.js → chunk-2MJ7EOMT.js} +20 -2
  13. package/dist/{chunk-R77O2WMK.js → chunk-2OIYNHWO.js} +1 -1
  14. package/dist/{chunk-QCN24XEN.js → chunk-3YUBFTDN.js} +1 -1
  15. package/dist/chunk-4FCL4DVZ.js +48 -0
  16. package/dist/{chunk-F7H6ATM4.js → chunk-5KXCM5AW.js} +18 -2
  17. package/dist/chunk-7KRIHYLR.js +124 -0
  18. package/dist/{chunk-RBNMDAKB.js → chunk-BCG45AQB.js} +15 -1
  19. package/dist/{chunk-QMJCNZ6C.js → chunk-C3KLHJDB.js} +1 -1
  20. package/dist/{chunk-H2X54EHC.js → chunk-DGGZVZTM.js} +1 -1
  21. package/dist/chunk-DW4TRDOZ.js +29 -0
  22. package/dist/{chunk-4JOQZPCE.js → chunk-EIQDZC43.js} +1 -1
  23. package/dist/{chunk-TQJ6ZYVY.js → chunk-FJ2MZ5PH.js} +41 -5
  24. package/dist/{chunk-BJZ4PV7R.js → chunk-HNGFKPNS.js} +26 -10
  25. package/dist/{chunk-5ZQZDDIK.js → chunk-HPGN6XRT.js} +3 -3
  26. package/dist/{chunk-KGIXX4NM.js → chunk-HUNDXZBZ.js} +5 -5
  27. package/dist/{chunk-ZDZISKCL.js → chunk-IJPMIEVE.js} +1 -1
  28. package/dist/chunk-IYCV34IU.js +158 -0
  29. package/dist/{chunk-ZNMCCHCP.js → chunk-LTLFOSA4.js} +2 -2
  30. package/dist/{chunk-K6T43YBD.js → chunk-MJRXFUMS.js} +26 -6
  31. package/dist/{chunk-4YU7HF4L.js → chunk-MOERAQXH.js} +6 -4
  32. package/dist/{chunk-KQSFTPIM.js → chunk-MTRMQFCQ.js} +1 -1
  33. package/dist/{chunk-OV7ZQ2GH.js → chunk-ODDGGLM6.js} +3 -3
  34. package/dist/{chunk-A7WQZKOU.js → chunk-OQJMTHEH.js} +2 -2
  35. package/dist/{chunk-RRNXJOKB.js → chunk-PJVPDY2Z.js} +2 -2
  36. package/dist/{chunk-FXORA5JG.js → chunk-Q7HOAVFR.js} +3 -3
  37. package/dist/{chunk-KDLYIECM.js → chunk-R772ET2M.js} +18 -4
  38. package/dist/{chunk-QLWG3C2Z.js → chunk-RRZ4RQQU.js} +2 -2
  39. package/dist/{mind-list-4D7J4CHD.js → chunk-SHKM5RGD.js} +18 -4
  40. package/dist/{chunk-RXEEYDIB.js → chunk-SZJCJVXV.js} +1 -1
  41. package/dist/{chunk-E3DA4HYV.js → chunk-T4VS7UDW.js} +15 -15
  42. package/dist/{chunk-YXSUMLTC.js → chunk-USD45JKR.js} +322 -76
  43. package/dist/{chunk-JO5IO4FH.js → chunk-VB2QXUJQ.js} +391 -294
  44. package/dist/{chunk-JZ62SYWR.js → chunk-VZRA5TYO.js} +1 -1
  45. package/dist/{chunk-PJ45FQPT.js → chunk-WARH6GIV.js} +8 -8
  46. package/dist/{chunk-PFZBNKUH.js → chunk-WCFSBH33.js} +0 -1
  47. package/dist/{chunk-DRJ24GXK.js → chunk-WQ46E33M.js} +59 -39
  48. package/dist/{chunk-ALKYQULU.js → chunk-WW6C3PLV.js} +1 -1
  49. package/dist/{chunk-CDPBTTZO.js → chunk-XVBSFLM7.js} +2 -2
  50. package/dist/{chunk-6EMWJZBK.js → chunk-YKA7DCVW.js} +1 -1
  51. package/dist/{chunk-IHKGWTGT.js → chunk-YNBXEQAP.js} +31 -1
  52. package/dist/cli.js +34 -26
  53. package/dist/{clock-RHS2JEDL.js → clock-CNRF6F7T.js} +3 -3
  54. package/dist/{cloud-sync-46RS4SDA.js → cloud-sync-WHKCB4CD.js} +22 -19
  55. package/dist/{config-TJLYUV3U.js → config-RPSLA2HX.js} +3 -3
  56. package/dist/{conversations-CJOOCGNW.js → conversations-C7IKYSHD.js} +4 -4
  57. package/dist/{create-IOV4GCRX.js → create-GJKHABJB.js} +16 -5
  58. package/dist/{create-DF3KISTW.js → create-VIQVP3QB.js} +1 -1
  59. package/dist/{daemon-client-IYR4KCPU.js → daemon-client-ZFY5MOAL.js} +5 -3
  60. package/dist/{daemon-restart-VFEI57UO.js → daemon-restart-7KUHVZIN.js} +7 -7
  61. package/dist/daemon.js +829 -496
  62. package/dist/{db-MF2JNRUW.js → db-FO6LXYCW.js} +2 -2
  63. package/dist/{db-LZTLA7XC.js → db-IPXRJUI7.js} +1 -1
  64. package/dist/{delete-HPLC7ETU.js → delete-ESEP7NGQ.js} +1 -1
  65. package/dist/delivery-manager-HZTT5SJR.js +36 -0
  66. package/dist/{delivery-router-N4A7ZC5T.js → delivery-router-3QFG2ZEX.js} +3 -3
  67. package/dist/{down-U2CZ23M4.js → down-G6IADJXV.js} +5 -5
  68. package/dist/echo-text-H3SH337Q.js +36 -0
  69. package/dist/env-6TGPO5WG.js +18 -0
  70. package/dist/{env-VZ4MLFQW.js → env-C7Z55QCJ.js} +6 -6
  71. package/dist/{exec-G3ZRST4Z.js → exec-QIFYZG4C.js} +3 -3
  72. package/dist/{export-LY723I3Z.js → export-C2K3NIRD.js} +4 -4
  73. package/dist/{extension-SEID47SR.js → extension-J4ZQ2GHV.js} +1 -1
  74. package/dist/{extensions-ZELQLTSR.js → extensions-GEKF47A4.js} +24 -19
  75. package/dist/{files-4LN3EN7Q.js → files-LXQSQOFG.js} +4 -4
  76. package/dist/{import-42YNQBF4.js → import-SX3ZXHB4.js} +8 -8
  77. package/dist/{isolation-YMGZZ4E5.js → isolation-MCT4ZYLT.js} +3 -3
  78. package/dist/{join-ZJV7W6MO.js → join-34TR2GXQ.js} +1 -1
  79. package/dist/{list-OXIAY3W7.js → list-PMUXZNAM.js} +1 -1
  80. package/dist/{login-MY6ETFYB.js → login-ARRZ4ORJ.js} +3 -3
  81. package/dist/{login-M6OJUPQ2.js → login-CL4IT2ED.js} +14 -5
  82. package/dist/{logout-WQBWO77G.js → logout-HZYJCYCA.js} +3 -3
  83. package/dist/{logout-NCG3TMDL.js → logout-YZRMZ3O2.js} +1 -1
  84. package/dist/message-delivery-GPQ5SE5N.js +40 -0
  85. package/dist/{mind-LHKS55DI.js → mind-46AOK373.js} +23 -19
  86. package/dist/{mind-activity-tracker-3QRNJVR4.js → mind-activity-tracker-YVZPC4A5.js} +4 -4
  87. package/dist/mind-contacts-GV7XYL5D.js +64 -0
  88. package/dist/{mind-history-VRO5A76U.js → mind-history-TTGA5BHZ.js} +5 -5
  89. package/dist/mind-list-OTHV4ZO4.js +13 -0
  90. package/dist/{mind-profile-PEQYASQX.js → mind-profile-LUG353ZT.js} +1 -1
  91. package/dist/mind-service-2ARGJNKP.js +44 -0
  92. package/dist/{mind-sleep-74TGOQBC.js → mind-sleep-DV7KP255.js} +3 -3
  93. package/dist/{mind-status-UO34ASXZ.js → mind-status-BJNPGXKC.js} +23 -1
  94. package/dist/{mind-wake-LCLGTXA2.js → mind-wake-LVTZ55EZ.js} +1 -1
  95. package/dist/notices-2KU5NTRY.js +25 -0
  96. package/dist/{package-DOD6CPO6.js → package-RTN2PS7O.js} +5 -3
  97. package/dist/{read-QVAXMTPV.js → read-YSWUXYL7.js} +1 -1
  98. package/dist/{register-Y2H4PY5R.js → register-42VBJKTP.js} +3 -3
  99. package/dist/{registry-OUZBZ3Q4.js → registry-UMMDRPRM.js} +2 -2
  100. package/dist/{reject-EV35X64Y.js → reject-SXGCYN4A.js} +1 -1
  101. package/dist/{restart-FJQ662XJ.js → restart-YYEGZJDD.js} +5 -5
  102. package/dist/{sandbox-ZJ7QA6Z6.js → sandbox-5VQ454XP.js} +4 -4
  103. package/dist/scheduler-I7JZCT47.js +34 -0
  104. package/dist/{schema-PVNG3PQM.js → schema-VEVO7HG6.js} +3 -1
  105. package/dist/{seed-CNYIHPOH.js → seed-F2HXZQU4.js} +1 -1
  106. package/dist/{seed-check-VLTVILOV.js → seed-check-TYD7RRZY.js} +1 -1
  107. package/dist/{seed-cmd-AVNUNLZV.js → seed-cmd-MLCTGSX5.js} +3 -3
  108. package/dist/{seed-create-ANXGYSCC.js → seed-create-KQ7OILWB.js} +6 -4
  109. package/dist/seed-readiness-RBPGOLSZ.js +18 -0
  110. package/dist/{seed-sprout-NIUFTX32.js → seed-sprout-HVYCFJLA.js} +30 -31
  111. package/dist/{send-SPE6DVNJ.js → send-TD2X5U3K.js} +6 -6
  112. package/dist/{service-UOCLXDWR.js → service-CVZMQYK6.js} +4 -4
  113. package/dist/{service-install-4HHFDDWY.js → service-install-VCRMFWX5.js} +5 -5
  114. package/dist/{setup-Y2EJI3GG.js → setup-5ECWTYKG.js} +7 -3
  115. package/dist/{setup-QT2A25SY.js → setup-AE4CVUC4.js} +7 -7
  116. package/dist/{skill-FKFHIJRR.js → skill-UUM4JZRF.js} +5 -5
  117. package/dist/skills/dreaming/SKILL.md +5 -1
  118. package/dist/skills/dreaming/scripts/dream.ts +5 -1
  119. package/dist/skills/orientation/SKILL.md +8 -7
  120. package/dist/skills/tending/SKILL.md +51 -2
  121. package/dist/skills/volute-mind/SKILL.md +2 -0
  122. package/dist/{skills-AKCFA7BN.js → skills-B3SVDCEB.js} +5 -5
  123. package/dist/sleep-manager-Z27R3WTW.js +38 -0
  124. package/dist/spirit-WXS3HCLN.js +25 -0
  125. package/dist/{split-WBMZXBHF.js → split-SKZ4Y2D6.js} +1 -1
  126. package/dist/{sprout-SMX42ZBP.js → sprout-KQ43BWZJ.js} +1 -1
  127. package/dist/{src-OJHARHTF.js → src-WLJE73NP.js} +14 -5
  128. package/dist/{start-5JE3TV66.js → start-O6UKK7BG.js} +3 -3
  129. package/dist/{status-SWRVGXBI.js → status-F4PGREUU.js} +19 -7
  130. package/dist/{stop-AAMTG3TU.js → stop-UBX6X5RA.js} +5 -5
  131. package/dist/system-chat-63ABFF6V.js +40 -0
  132. package/dist/{systems-VNB5WKSS.js → systems-JJNGQHAO.js} +4 -4
  133. package/dist/{tailscale-3XOKOW7T.js → tailscale-MCLY5GYE.js} +2 -2
  134. package/dist/{template-hash-446HHH5V.js → template-hash-GSOUARW4.js} +2 -2
  135. package/dist/template-staleness-EMQORSGG.js +15 -0
  136. package/dist/{turn-tracker-FOIBNUUA.js → turn-tracker-KLPV3PAU.js} +6 -6
  137. package/dist/up-RVGDE6CQ.js +20 -0
  138. package/dist/{update-SWAYZRWJ.js → update-OXQEGGXD.js} +5 -5
  139. package/dist/{update-check-PQ2MZ47K.js → update-check-266R4ZSY.js} +3 -3
  140. package/dist/{upgrade-NWAZ7MLD.js → upgrade-K5FIQPQD.js} +1 -1
  141. package/dist/{version-notify-C5CH7CVP.js → version-notify-73YURTFR.js} +41 -25
  142. package/dist/web-assets/assets/index-WdZ60R6c.css +1 -0
  143. package/dist/web-assets/assets/index-taX6Ezfa.js +92 -0
  144. package/dist/web-assets/index.html +2 -2
  145. package/drizzle/0011_drop_turn_summary_event_id.sql +3 -0
  146. package/drizzle/0012_channel_gates.sql +17 -0
  147. package/drizzle/meta/_journal.json +14 -0
  148. package/package.json +5 -3
  149. package/packages/extensions/pages/dist/ui/assets/index-BuXnE-rb.css +1 -0
  150. package/packages/extensions/pages/dist/ui/assets/index-DtzftuPA.js +2 -0
  151. package/packages/extensions/pages/dist/ui/index.html +2 -2
  152. package/templates/_base/src/lib/startup.ts +59 -3
  153. package/templates/claude/src/agent.ts +37 -4
  154. package/templates/claude/src/lib/session-reaper.ts +32 -0
  155. package/templates/claude/src/server.ts +14 -3
  156. package/templates/pi/src/agent.ts +7 -10
  157. package/templates/pi/src/lib/dispatch.ts +65 -0
  158. package/dist/avatar-image-6KY6UU4D.js +0 -16
  159. package/dist/delivery-manager-JINZRG2W.js +0 -33
  160. package/dist/echo-text-VQPB5L2X.js +0 -33
  161. package/dist/message-delivery-R7JSQGYX.js +0 -37
  162. package/dist/mind-service-NB2JMWQZ.js +0 -39
  163. package/dist/scheduler-WVKII3LA.js +0 -31
  164. package/dist/sleep-manager-G6TXSDP7.js +0 -35
  165. package/dist/spirit-RPBLLUQ5.js +0 -23
  166. package/dist/system-chat-NDAW6YP7.js +0 -37
  167. package/dist/up-PS6PFS75.js +0 -18
  168. package/dist/volute-config-A52FHWKM.js +0 -14
  169. package/dist/web-assets/assets/index-B1JBQv2m.css +0 -1
  170. package/dist/web-assets/assets/index-CU6XWICM.js +0 -86
  171. package/packages/extensions/pages/dist/ui/assets/index-BcY6lnq4.css +0 -1
  172. package/packages/extensions/pages/dist/ui/assets/index-CQjEQg1r.js +0 -2
@@ -1,82 +1,92 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- loadMergedEnv
4
- } from "./chunk-ZDZISKCL.js";
5
2
  import {
6
3
  spiritDir
7
- } from "./chunk-BJZ4PV7R.js";
4
+ } from "./chunk-HNGFKPNS.js";
5
+ import {
6
+ clearMind,
7
+ linkInboundToActiveTurn,
8
+ publish as publish3,
9
+ summarizeOrphanedTurns
10
+ } from "./chunk-USD45JKR.js";
11
+ import {
12
+ getPrompt
13
+ } from "./chunk-MOERAQXH.js";
8
14
  import {
9
15
  hashSkillDir,
10
16
  importSkillFromDir,
11
17
  removeSharedSkill,
12
18
  sharedSkillsDir
13
- } from "./chunk-KGIXX4NM.js";
19
+ } from "./chunk-HUNDXZBZ.js";
20
+ import {
21
+ getOrCreateMindUser,
22
+ getOrCreateSystemUser,
23
+ getUser,
24
+ getUserByUsername,
25
+ syncMindProfile
26
+ } from "./chunk-ODDGGLM6.js";
14
27
  import {
15
28
  addMessage,
16
29
  createChannel,
17
30
  createConversation,
18
31
  findDMConversation,
19
32
  getChannelByName,
33
+ getChannelSettings,
20
34
  getParticipants,
21
35
  joinChannel,
22
- publish as publish2
23
- } from "./chunk-5ZQZDDIK.js";
24
- import {
25
- readVoluteConfig,
26
- resolveWakeTriggers,
27
- writeVoluteConfig
28
- } from "./chunk-PFZBNKUH.js";
36
+ publish as publish2,
37
+ updateChannelSettings
38
+ } from "./chunk-HPGN6XRT.js";
29
39
  import {
30
40
  isSandboxEnabled,
31
41
  wrapForSandbox
32
- } from "./chunk-JZ62SYWR.js";
42
+ } from "./chunk-VZRA5TYO.js";
43
+ import {
44
+ MIND_LEVEL_SESSION,
45
+ recordNotice
46
+ } from "./chunk-IYCV34IU.js";
33
47
  import {
34
48
  markIdle,
35
49
  onMindEvent
36
- } from "./chunk-KQSFTPIM.js";
50
+ } from "./chunk-MTRMQFCQ.js";
51
+ import {
52
+ renderAvatarBlock
53
+ } from "./chunk-WQ46E33M.js";
54
+ import {
55
+ readVoluteConfig,
56
+ resolveWakeTriggers,
57
+ writeVoluteConfig
58
+ } from "./chunk-WCFSBH33.js";
37
59
  import {
38
60
  extractTextContent,
39
61
  getRoutingConfig,
40
62
  resolveDeliveryMode,
41
63
  resolveRoute,
42
64
  setRoutesChangeListener
43
- } from "./chunk-4JOQZPCE.js";
44
- import {
45
- clearMind,
46
- linkInboundToActiveTurn,
47
- publish as publish3,
48
- summarizeOrphanedTurns
49
- } from "./chunk-YXSUMLTC.js";
50
- import {
51
- getPrompt
52
- } from "./chunk-4YU7HF4L.js";
65
+ } from "./chunk-EIQDZC43.js";
53
66
  import {
54
67
  chownMindDir,
55
68
  exec,
56
69
  isIsolationEnabled,
57
70
  mindUserName,
58
71
  wrapForIsolation
59
- } from "./chunk-H2X54EHC.js";
72
+ } from "./chunk-DGGZVZTM.js";
73
+ import {
74
+ loadMergedEnv
75
+ } from "./chunk-IJPMIEVE.js";
60
76
  import {
61
77
  aiCompleteUtility,
62
78
  getAiConfig,
79
+ missingCredentialWarning,
63
80
  resolveApiKey
64
- } from "./chunk-IHKGWTGT.js";
81
+ } from "./chunk-YNBXEQAP.js";
65
82
  import {
66
83
  readGlobalConfig,
67
84
  writeGlobalConfig
68
- } from "./chunk-F7H6ATM4.js";
69
- import {
70
- getOrCreateMindUser,
71
- getOrCreateSystemUser,
72
- getUser,
73
- getUserByUsername,
74
- syncMindProfile
75
- } from "./chunk-OV7ZQ2GH.js";
85
+ } from "./chunk-5KXCM5AW.js";
76
86
  import {
77
87
  publish,
78
88
  subscribe
79
- } from "./chunk-ZNMCCHCP.js";
89
+ } from "./chunk-LTLFOSA4.js";
80
90
  import {
81
91
  logger_default
82
92
  } from "./chunk-T2TP6ZC6.js";
@@ -88,38 +98,39 @@ import {
88
98
  isSpiritName,
89
99
  mindDir,
90
100
  mindTmpDir,
101
+ readRegistry,
91
102
  setMindRunning,
92
103
  stateDir,
93
104
  voluteHome,
94
105
  voluteSystemDir
95
- } from "./chunk-ALKYQULU.js";
106
+ } from "./chunk-WW6C3PLV.js";
96
107
  import {
97
108
  activity,
109
+ channelGates,
98
110
  deliveryQueue,
99
111
  messages,
100
112
  mindHistory,
101
- mindNotices,
102
113
  sessions
103
- } from "./chunk-ZV7MFISX.js";
114
+ } from "./chunk-2MJ7EOMT.js";
104
115
 
105
116
  // packages/daemon/src/lib/delivery/message-delivery.ts
106
- import { and as and5, eq as eq6, inArray as inArray3 } from "drizzle-orm";
117
+ import { and as and4, eq as eq5, inArray as inArray3 } from "drizzle-orm";
107
118
 
108
119
  // packages/daemon/src/lib/daemon/sleep-manager.ts
109
120
  import { execFile as execFile2, spawn as spawnChild } from "child_process";
110
121
  import {
111
- existsSync as existsSync8,
122
+ existsSync as existsSync9,
112
123
  mkdirSync as mkdirSync5,
113
124
  readdirSync as readdirSync2,
114
- readFileSync as readFileSync7,
125
+ readFileSync as readFileSync8,
115
126
  readlinkSync,
116
127
  renameSync,
117
128
  writeFileSync as writeFileSync6
118
129
  } from "fs";
119
- import { resolve as resolve7 } from "path";
130
+ import { resolve as resolve8 } from "path";
120
131
  import { promisify as promisify2 } from "util";
121
132
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
122
- import { and as and2, eq as eq3, inArray } from "drizzle-orm";
133
+ import { and, eq as eq2, inArray } from "drizzle-orm";
123
134
 
124
135
  // packages/daemon/src/lib/daemon/mind-manager.ts
125
136
  import { execFile, spawn } from "child_process";
@@ -344,106 +355,6 @@ function getMindToken(mindName) {
344
355
  return mindToToken.get(mindName) ?? null;
345
356
  }
346
357
 
347
- // packages/daemon/src/lib/daemon/notices.ts
348
- import { and, asc, eq, lte, or, sql } from "drizzle-orm";
349
- var nlog = logger_default.child("notices");
350
- var MIND_LEVEL_SESSION = "";
351
- var MAX_NOTICES_PER_SESSION = 100;
352
- async function recordNotice(input) {
353
- try {
354
- const db = await getDb();
355
- await db.insert(mindNotices).values({
356
- mind: input.mind,
357
- session: input.session,
358
- kind: input.kind,
359
- reason: input.reason,
360
- detail: input.detail,
361
- raw: input.raw ?? null
362
- });
363
- await db.run(
364
- sql`DELETE FROM mind_notices WHERE mind = ${input.mind} AND session = ${input.session} AND id NOT IN (SELECT id FROM mind_notices WHERE mind = ${input.mind} AND session = ${input.session} ORDER BY id DESC LIMIT ${MAX_NOTICES_PER_SESSION})`
365
- );
366
- } catch (err) {
367
- nlog.error(`failed to record notice for ${input.mind}:${input.session}`, logger_default.errorData(err));
368
- }
369
- }
370
- async function drainNotices(mind, session, limit = MAX_NOTICES_PER_SESSION) {
371
- const db = await getDb();
372
- return db.select().from(mindNotices).where(
373
- and(
374
- eq(mindNotices.mind, mind),
375
- or(eq(mindNotices.session, session), eq(mindNotices.session, MIND_LEVEL_SESSION))
376
- )
377
- ).orderBy(asc(mindNotices.id)).limit(limit);
378
- }
379
- async function clearDeliveredNotices(mind, session, uptoId) {
380
- try {
381
- const db = await getDb();
382
- await db.delete(mindNotices).where(
383
- and(
384
- eq(mindNotices.mind, mind),
385
- or(eq(mindNotices.session, session), eq(mindNotices.session, MIND_LEVEL_SESSION)),
386
- lte(mindNotices.id, uptoId)
387
- )
388
- );
389
- } catch (err) {
390
- nlog.warn(`failed to clear delivered notices for ${mind}:${session}`, logger_default.errorData(err));
391
- }
392
- }
393
- var NOTICE_HEADER = "[Notices] While you were unavailable, one or more turns failed. You're back now:";
394
- function formatNotices(notices) {
395
- if (notices.length === 0) return null;
396
- const failures = notices.filter((n) => n.kind !== "extension" && n.kind !== "budget");
397
- const budgets = notices.filter((n) => n.kind === "budget");
398
- const extensions = notices.filter((n) => n.kind === "extension");
399
- const blocks = [];
400
- if (failures.length > 0) {
401
- const groups = /* @__PURE__ */ new Map();
402
- for (const n of failures) {
403
- const time = localHM(n.created_at);
404
- const g = groups.get(n.reason);
405
- if (g) {
406
- g.count += 1;
407
- g.detail = n.detail;
408
- g.last = time;
409
- } else {
410
- groups.set(n.reason, { count: 1, detail: n.detail, first: time, last: time });
411
- }
412
- }
413
- const lines = [...groups.values()].map((g) => {
414
- const span = g.first === g.last ? g.first : `${g.first}\u2013${g.last}`;
415
- const plural = g.count === 1 ? "turn" : "turns";
416
- return `- ${g.count} ${plural} failed (${span}): ${g.detail}`;
417
- });
418
- blocks.push(`${NOTICE_HEADER}
419
- ${lines.join("\n")}`);
420
- }
421
- if (budgets.length > 0) {
422
- const lines = budgets.map((n) => `- ${localHM(n.created_at)} ${n.detail}`);
423
- blocks.push(`[Budget]
424
- ${lines.join("\n")}`);
425
- }
426
- if (extensions.length > 0) {
427
- const byExt = /* @__PURE__ */ new Map();
428
- for (const n of extensions) {
429
- const arr = byExt.get(n.reason);
430
- if (arr) arr.push(n);
431
- else byExt.set(n.reason, [n]);
432
- }
433
- for (const [ext, items] of byExt) {
434
- const header = `[${ext.charAt(0).toUpperCase()}${ext.slice(1)}]`;
435
- const lines = items.map((n) => `- ${localHM(n.created_at)} ${n.detail}`);
436
- blocks.push(`${header}
437
- ${lines.join("\n")}`);
438
- }
439
- }
440
- return blocks.join("\n\n");
441
- }
442
- function localHM(createdAt) {
443
- const iso = createdAt.endsWith("Z") ? createdAt : `${createdAt.replace(" ", "T")}Z`;
444
- return new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
445
- }
446
-
447
358
  // packages/daemon/src/lib/daemon/restart-tracker.ts
448
359
  var DEFAULT_MAX_ATTEMPTS = 5;
449
360
  var DEFAULT_BASE_DELAY = 3e3;
@@ -804,7 +715,7 @@ var MindManager = class {
804
715
  while (recentStderr.length > 20) recentStderr.shift();
805
716
  });
806
717
  try {
807
- await new Promise((resolve9, reject) => {
718
+ await new Promise((resolve10, reject) => {
808
719
  let settled = false;
809
720
  const deadline = Date.now() + 3e4;
810
721
  const onExit = (code) => {
@@ -834,7 +745,7 @@ var MindManager = class {
834
745
  const { ok } = await checkHealth(port);
835
746
  if (settled) return;
836
747
  if (ok) {
837
- finish(() => resolve9());
748
+ finish(() => resolve10());
838
749
  return;
839
750
  }
840
751
  if (Date.now() >= deadline) {
@@ -934,7 +845,7 @@ var MindManager = class {
934
845
  if (this.shuttingDown || this.stopping.has(name)) return;
935
846
  mlog.error(`mind ${name} exited with code ${code}`);
936
847
  try {
937
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
848
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-Z27R3WTW.js");
938
849
  const sleepMgr = getSleepManagerIfReady2();
939
850
  const sleepState = sleepMgr?.getState(name);
940
851
  if (sleepState?.sleeping) {
@@ -964,15 +875,15 @@ var MindManager = class {
964
875
  (err) => mlog.warn(`failed to clear turn state for ${name} after crash`, logger_default.errorData(err))
965
876
  );
966
877
  try {
967
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-JINZRG2W.js");
878
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-HZTT5SJR.js");
968
879
  getDeliveryManager2().clearMindSessions(name);
969
880
  } catch (err) {
970
881
  if (!(err instanceof Error && err.message.includes("not initialized"))) {
971
882
  mlog.warn(`failed to clear delivery state for ${name} after crash`, logger_default.errorData(err));
972
883
  }
973
884
  }
974
- import("./mind-activity-tracker-3QRNJVR4.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
975
- import("./activity-events-NF6VWTJQ.js").then(
885
+ import("./mind-activity-tracker-YVZPC4A5.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
886
+ import("./activity-events-MIOK3IOU.js").then(
976
887
  ({ publish: publish4 }) => publish4({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
977
888
  ).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
978
889
  const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
@@ -1002,23 +913,23 @@ var MindManager = class {
1002
913
  this.stopping.add(name);
1003
914
  const { child } = tracked;
1004
915
  this.minds.delete(name);
1005
- await new Promise((resolve9) => {
916
+ await new Promise((resolve10) => {
1006
917
  const killTimer = setTimeout(() => {
1007
918
  try {
1008
919
  process.kill(-child.pid, "SIGKILL");
1009
920
  } catch {
1010
921
  }
1011
- resolve9();
922
+ resolve10();
1012
923
  }, 5e3);
1013
924
  child.on("exit", () => {
1014
925
  clearTimeout(killTimer);
1015
- resolve9();
926
+ resolve10();
1016
927
  });
1017
928
  try {
1018
929
  process.kill(-child.pid, "SIGTERM");
1019
930
  } catch {
1020
931
  clearTimeout(killTimer);
1021
- resolve9();
932
+ resolve10();
1022
933
  }
1023
934
  });
1024
935
  this.stopping.delete(name);
@@ -1030,7 +941,7 @@ var MindManager = class {
1030
941
  mlog.warn(`failed to clear turn state for ${name} on stop`, logger_default.errorData(err));
1031
942
  }
1032
943
  try {
1033
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-JINZRG2W.js");
944
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-HZTT5SJR.js");
1034
945
  getDeliveryManager2().clearMindSessions(name);
1035
946
  } catch (err) {
1036
947
  if (!(err instanceof Error && err.message.includes("not initialized"))) {
@@ -1038,7 +949,7 @@ var MindManager = class {
1038
949
  }
1039
950
  }
1040
951
  try {
1041
- const { clearEchoTextCache } = await import("./echo-text-VQPB5L2X.js");
952
+ const { clearEchoTextCache } = await import("./echo-text-H3SH337Q.js");
1042
953
  clearEchoTextCache(name);
1043
954
  } catch (err) {
1044
955
  mlog.debug(`failed to clear echo-text cache for ${name}`, logger_default.errorData(err));
@@ -1122,17 +1033,30 @@ function tryGetMindManager() {
1122
1033
  return instance;
1123
1034
  }
1124
1035
 
1036
+ // packages/daemon/src/lib/daemon/mind-service.ts
1037
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
1038
+ import { resolve as resolve7 } from "path";
1039
+
1125
1040
  // packages/daemon/src/lib/chat/system-channel.ts
1126
1041
  var SYSTEM_CHANNEL_NAME = "system";
1042
+ 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.";
1127
1043
  var cachedChannelId = null;
1128
1044
  async function ensureSystemChannel() {
1129
1045
  if (cachedChannelId) return cachedChannelId;
1130
1046
  const existing = await getChannelByName(SYSTEM_CHANNEL_NAME);
1131
1047
  if (existing) {
1132
1048
  cachedChannelId = existing.id;
1049
+ const settings = await getChannelSettings(SYSTEM_CHANNEL_NAME);
1050
+ if (settings && settings.description == null) {
1051
+ await updateChannelSettings(SYSTEM_CHANNEL_NAME, {
1052
+ description: SYSTEM_CHANNEL_DESCRIPTION
1053
+ });
1054
+ }
1133
1055
  return existing.id;
1134
1056
  }
1135
- const conv = await createChannel(SYSTEM_CHANNEL_NAME);
1057
+ const conv = await createChannel(SYSTEM_CHANNEL_NAME, void 0, {
1058
+ description: SYSTEM_CHANNEL_DESCRIPTION
1059
+ });
1136
1060
  cachedChannelId = conv.id;
1137
1061
  logger_default.info("created #system channel");
1138
1062
  return conv.id;
@@ -1145,6 +1069,25 @@ async function joinSystemChannelForMind(mindName) {
1145
1069
  const user = await getOrCreateMindUser(mindName);
1146
1070
  await joinSystemChannel(user.id);
1147
1071
  }
1072
+ async function joinSystemChannelForSpirit() {
1073
+ const user = await getOrCreateSystemUser();
1074
+ await joinSystemChannel(user.id);
1075
+ }
1076
+ async function backfillSystemChannelMembers() {
1077
+ const entries = await readRegistry();
1078
+ for (const entry of entries) {
1079
+ if (entry.stage === "seed") continue;
1080
+ try {
1081
+ if (entry.mindType === "spirit") {
1082
+ await joinSystemChannelForSpirit();
1083
+ } else {
1084
+ await joinSystemChannelForMind(entry.name);
1085
+ }
1086
+ } catch (err) {
1087
+ logger_default.error(`failed to backfill ${entry.name} into #system`, logger_default.errorData(err));
1088
+ }
1089
+ }
1090
+ }
1148
1091
  async function announceToSystem(text) {
1149
1092
  const channelId = await ensureSystemChannel();
1150
1093
  const systemUser = await getOrCreateSystemUser();
@@ -1174,7 +1117,7 @@ import { dirname, resolve as resolve4 } from "path";
1174
1117
 
1175
1118
  // packages/daemon/src/web/middleware/auth.ts
1176
1119
  import { timingSafeEqual } from "crypto";
1177
- import { eq as eq2, lt } from "drizzle-orm";
1120
+ import { eq, lt } from "drizzle-orm";
1178
1121
  import { getCookie } from "hono/cookie";
1179
1122
  import { createMiddleware } from "hono/factory";
1180
1123
  var MIND_USER_CACHE_TTL = 5 * 60 * 1e3;
@@ -1202,14 +1145,14 @@ async function createSession(userId) {
1202
1145
  async function deleteSession(sessionId) {
1203
1146
  sessionCache.delete(sessionId);
1204
1147
  const db = await getDb();
1205
- await db.delete(sessions).where(eq2(sessions.id, sessionId));
1148
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
1206
1149
  }
1207
1150
  async function getSessionUserId(sessionId) {
1208
1151
  const db = await getDb();
1209
- const row = await db.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1152
+ const row = await db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
1210
1153
  if (!row) return void 0;
1211
1154
  if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
1212
- await db.delete(sessions).where(eq2(sessions.id, sessionId));
1155
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
1213
1156
  return void 0;
1214
1157
  }
1215
1158
  return row.userId;
@@ -1362,10 +1305,54 @@ function deleteSystemsConfig() {
1362
1305
  }
1363
1306
  }
1364
1307
 
1308
+ // packages/daemon/src/lib/util/sanitize-svg.ts
1309
+ import DOMPurify from "isomorphic-dompurify";
1310
+ var ALLOWED_TAGS = ["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g"];
1311
+ var ALLOWED_ATTR = [
1312
+ "viewBox",
1313
+ "fill",
1314
+ "fill-opacity",
1315
+ "fill-rule",
1316
+ "stroke",
1317
+ "stroke-width",
1318
+ "stroke-linecap",
1319
+ "stroke-linejoin",
1320
+ "stroke-dasharray",
1321
+ "stroke-opacity",
1322
+ "opacity",
1323
+ "d",
1324
+ "cx",
1325
+ "cy",
1326
+ "r",
1327
+ "x",
1328
+ "y",
1329
+ "width",
1330
+ "height",
1331
+ "points",
1332
+ "transform"
1333
+ ];
1334
+ function sanitizeSvgIcon(html) {
1335
+ return DOMPurify.sanitize(html, {
1336
+ ALLOWED_TAGS,
1337
+ ALLOWED_ATTR,
1338
+ FORBID_TAGS: ["script", "foreignObject"],
1339
+ FORBID_ATTR: ["href", "xlink:href"]
1340
+ });
1341
+ }
1342
+
1365
1343
  // packages/daemon/src/lib/extensions.ts
1366
1344
  var VALID_EXTENSION_ID = /^[a-z0-9][a-z0-9_-]*$/;
1367
1345
  var loaded = [];
1368
1346
  var discovered = [];
1347
+ function enrichActivityMetadata(manifest, metadata) {
1348
+ const enriched = {
1349
+ ...metadata,
1350
+ ...manifest.icon && !metadata?.icon ? { icon: manifest.icon } : {},
1351
+ ...manifest.color && !metadata?.color ? { color: manifest.color } : {}
1352
+ };
1353
+ if (typeof enriched.icon === "string") enriched.icon = sanitizeSvgIcon(enriched.icon);
1354
+ return enriched;
1355
+ }
1369
1356
  function toCommandInfo(cmd) {
1370
1357
  const { handler: _, ...info } = cmd;
1371
1358
  return info;
@@ -1469,14 +1456,8 @@ async function buildContext(manifest, dataDir, authMw) {
1469
1456
  getUser: async (id) => getUser(id),
1470
1457
  getUserByUsername: async (username) => getUserByUsername(username),
1471
1458
  publishActivity: (event) => {
1472
- const enriched = {
1473
- ...event,
1474
- metadata: {
1475
- ...event.metadata,
1476
- ...manifest.icon && !event.metadata?.icon ? { icon: manifest.icon } : {},
1477
- ...manifest.color && !event.metadata?.color ? { color: manifest.color } : {}
1478
- }
1479
- };
1459
+ const metadata = enrichActivityMetadata(manifest, event.metadata);
1460
+ const enriched = { ...event, metadata };
1480
1461
  publish(enriched).catch(
1481
1462
  (err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
1482
1463
  );
@@ -1554,14 +1535,8 @@ async function loadExtension(manifest, app, authMw) {
1554
1535
  const result = await cmd.handler(parsed, {
1555
1536
  ...context,
1556
1537
  publishActivity: (rawEvent) => {
1557
- const event = {
1558
- ...rawEvent,
1559
- metadata: {
1560
- ...rawEvent.metadata,
1561
- ...manifest.icon && !rawEvent.metadata?.icon ? { icon: manifest.icon } : {},
1562
- ...manifest.color && !rawEvent.metadata?.color ? { color: manifest.color } : {}
1563
- }
1564
- };
1538
+ const metadata = enrichActivityMetadata(manifest, rawEvent.metadata);
1539
+ const event = { ...rawEvent, metadata };
1565
1540
  activityPromises.push(
1566
1541
  publish(event).catch((err) => {
1567
1542
  logger_default.error(
@@ -1601,7 +1576,7 @@ async function loadExtension(manifest, app, authMw) {
1601
1576
  }
1602
1577
  if (resolvedAssetsDir && existsSync6(resolvedAssetsDir)) {
1603
1578
  const assetsDir = resolvedAssetsDir;
1604
- const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
1579
+ const { readFile, stat: fsStat } = await import("fs/promises");
1605
1580
  const { extname: ext } = await import("path");
1606
1581
  const mimeTypes = {
1607
1582
  ".html": "text/html",
@@ -1626,11 +1601,11 @@ async function loadExtension(manifest, app, authMw) {
1626
1601
  const s = await fsStat(filePath).catch(() => null);
1627
1602
  if (s?.isFile()) {
1628
1603
  const mime = mimeTypes[ext(filePath)] || "application/octet-stream";
1629
- const body = await readFile2(filePath);
1604
+ const body = await readFile(filePath);
1630
1605
  return c.body(body, 200, { "Content-Type": mime });
1631
1606
  }
1632
1607
  if (existsSync6(indexPath)) {
1633
- const body = await readFile2(indexPath, "utf-8");
1608
+ const body = await readFile(indexPath, "utf-8");
1634
1609
  return c.html(body);
1635
1610
  }
1636
1611
  return c.text("Not found", 404);
@@ -1698,7 +1673,7 @@ function resolveSkillsDir(manifest) {
1698
1673
  async function discoverBuiltinExtensions(disabledIds) {
1699
1674
  const builtins = [
1700
1675
  { id: "notes", load: async () => (await import("./src-YH6PQ7LJ.js")).default },
1701
- { id: "pages", load: async () => (await import("./src-OJHARHTF.js")).default },
1676
+ { id: "pages", load: async () => (await import("./src-WLJE73NP.js")).default },
1702
1677
  { id: "plan", load: async () => (await import("./src-JAOVXO5W.js")).default }
1703
1678
  ];
1704
1679
  const results = [];
@@ -1948,7 +1923,7 @@ async function installNpmExtension(pkg) {
1948
1923
  throw new Error(`Extension "${pkg}" is already installed`);
1949
1924
  }
1950
1925
  const dir = ensureExtensionsNpmDir();
1951
- const { exec: exec2 } = await import("./exec-G3ZRST4Z.js");
1926
+ const { exec: exec2 } = await import("./exec-QIFYZG4C.js");
1952
1927
  try {
1953
1928
  await exec2("npm", ["install", "--ignore-scripts", pkg], { cwd: dir });
1954
1929
  } catch (err) {
@@ -1969,7 +1944,7 @@ async function uninstallNpmExtension(pkg) {
1969
1944
  packages.splice(idx, 1);
1970
1945
  writeExtensionsConfig(packages);
1971
1946
  try {
1972
- const { exec: exec2 } = await import("./exec-G3ZRST4Z.js");
1947
+ const { exec: exec2 } = await import("./exec-QIFYZG4C.js");
1973
1948
  await exec2("npm", ["uninstall", "--ignore-scripts", pkg], { cwd: extensionsNpmDir() });
1974
1949
  } catch (err) {
1975
1950
  logger_default.warn(
@@ -2431,10 +2406,17 @@ var Scheduler = class {
2431
2406
  ${stderr}` : ""}`;
2432
2407
  slog2.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
2433
2408
  }
2409
+ } else if (Array.isArray(schedule.messages) && schedule.messages.length > 0) {
2410
+ const pick = schedule.messages[Math.floor(Math.random() * schedule.messages.length)];
2411
+ if (typeof pick !== "string" || !pick.trim()) {
2412
+ slog2.warn(`schedule "${schedule.id}" for ${mindName} has a malformed messages pool`);
2413
+ return;
2414
+ }
2415
+ text = pick;
2434
2416
  } else if (schedule.message) {
2435
2417
  text = schedule.message;
2436
2418
  } else {
2437
- slog2.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
2419
+ slog2.warn(`schedule "${schedule.id}" for ${mindName} has no message, messages, or script`);
2438
2420
  return;
2439
2421
  }
2440
2422
  await this.deliverSystem(mindName, `[${schedule.id}] ${text}`, {
@@ -2754,6 +2736,9 @@ async function startMindFull(name) {
2754
2736
  summary: `${name} started`
2755
2737
  }).catch((err) => logger_default.error("failed to publish mind_started activity", logger_default.errorData(err)));
2756
2738
  if (entry?.parent) return;
2739
+ recordMissingCredentialsNotice(baseName, entry?.template).catch(
2740
+ (err) => logger_default.error(`failed to check credentials for ${baseName}`, logger_default.errorData(err))
2741
+ );
2757
2742
  if (!entry || entry.stage === "seed") {
2758
2743
  if (entry?.stage === "seed") {
2759
2744
  sendSeedOrientation(baseName, entry.createdBy).catch(
@@ -2798,6 +2783,31 @@ async function startMindFull(name) {
2798
2783
  }
2799
2784
  notifyExtensionsMindStart(baseName);
2800
2785
  }
2786
+ async function recordMissingCredentialsNotice(mind, template) {
2787
+ const { hasUndeliveredNotice, recordNotice: recordNotice2, MIND_LEVEL_SESSION: MIND_LEVEL_SESSION2 } = await import("./notices-2KU5NTRY.js");
2788
+ const warning = await missingCredentialWarning(template, readMindModel(mind, template), mind);
2789
+ if (!warning) return;
2790
+ if (await hasUndeliveredNotice(mind, "no_credentials")) return;
2791
+ await recordNotice2({
2792
+ mind,
2793
+ session: MIND_LEVEL_SESSION2,
2794
+ kind: "startup",
2795
+ reason: "no_credentials",
2796
+ detail: warning
2797
+ });
2798
+ }
2799
+ function readMindModel(mind, template) {
2800
+ if (template !== "pi") return void 0;
2801
+ try {
2802
+ const configPath2 = resolve7(mindDir(mind), "home/.config/config.json");
2803
+ if (!existsSync8(configPath2)) return void 0;
2804
+ const model = JSON.parse(readFileSync7(configPath2, "utf-8")).model;
2805
+ return typeof model === "string" ? model : void 0;
2806
+ } catch (err) {
2807
+ logger_default.warn(`failed to read model for ${mind}`, logger_default.errorData(err));
2808
+ return void 0;
2809
+ }
2810
+ }
2801
2811
  async function sleepMind(name) {
2802
2812
  markIdle(name);
2803
2813
  await getMindManager().stopMind(name);
@@ -2818,10 +2828,13 @@ async function wakeMind(name) {
2818
2828
  async function startSpiritFull(name) {
2819
2829
  const entry = await findMind(name);
2820
2830
  if (entry?.dir) {
2821
- const { registerMindDir } = await import("./delivery-router-N4A7ZC5T.js");
2831
+ const { registerMindDir } = await import("./delivery-router-3QFG2ZEX.js");
2822
2832
  registerMindDir(name, entry.dir);
2823
2833
  }
2824
2834
  await getMindManager().startMind(name);
2835
+ joinSystemChannelForSpirit().catch(
2836
+ (err) => logger_default.error(`failed to join #system for ${name}`, logger_default.errorData(err))
2837
+ );
2825
2838
  getScheduler().loadSchedules(name, entry?.dir ?? spiritDir());
2826
2839
  publish({
2827
2840
  type: "mind_started",
@@ -2848,14 +2861,14 @@ function buildSeedOrientation(createdBy, creatorType) {
2848
2861
  async function sendSeedOrientation(mindName, createdBy) {
2849
2862
  let creatorType;
2850
2863
  if (createdBy) {
2851
- const { getUserByUsername: getUserByUsername2 } = await import("./auth-CCFAU4GQ.js");
2864
+ const { getUserByUsername: getUserByUsername2 } = await import("./auth-A4VGHADI.js");
2852
2865
  creatorType = (await getUserByUsername2(createdBy))?.user_type;
2853
2866
  }
2854
2867
  await sendSystemMessage(mindName, buildSeedOrientation(createdBy, creatorType));
2855
2868
  }
2856
2869
  async function ensureCreatorDM(mindName, creatorUsername) {
2857
- const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-CCFAU4GQ.js");
2858
- const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-CJOOCGNW.js");
2870
+ const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-A4VGHADI.js");
2871
+ const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-C7IKYSHD.js");
2859
2872
  const mindUser = await getOrCreateMindUser2(mindName);
2860
2873
  const creatorUser = await getUserByUsername2(creatorUsername);
2861
2874
  if (!creatorUser) {
@@ -2951,7 +2964,7 @@ var SleepManager = class {
2951
2964
  transitioning = /* @__PURE__ */ new Set();
2952
2965
  sleepConfigs = /* @__PURE__ */ new Map();
2953
2966
  get statePath() {
2954
- return resolve7(voluteSystemDir(), "sleep-state.json");
2967
+ return resolve8(voluteSystemDir(), "sleep-state.json");
2955
2968
  }
2956
2969
  start() {
2957
2970
  this.loadState();
@@ -2967,8 +2980,8 @@ var SleepManager = class {
2967
2980
  // --- State persistence ---
2968
2981
  loadState() {
2969
2982
  try {
2970
- if (existsSync8(this.statePath)) {
2971
- const data = JSON.parse(readFileSync7(this.statePath, "utf-8"));
2983
+ if (existsSync9(this.statePath)) {
2984
+ const data = JSON.parse(readFileSync8(this.statePath, "utf-8"));
2972
2985
  for (const [name, state2] of Object.entries(data)) {
2973
2986
  state2.triggerWakeHistory ??= [];
2974
2987
  state2.wakeFailures ??= 0;
@@ -3259,7 +3272,7 @@ var SleepManager = class {
3259
3272
  async flushQueuedMessages(name) {
3260
3273
  try {
3261
3274
  const db = await getDb();
3262
- const rows = await db.select().from(deliveryQueue).where(and2(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
3275
+ const rows = await db.select().from(deliveryQueue).where(and(eq2(deliveryQueue.mind, name), eq2(deliveryQueue.status, "sleep-queued"))).all();
3263
3276
  if (rows.length === 0) return 0;
3264
3277
  const groups = /* @__PURE__ */ new Map();
3265
3278
  for (const row of rows) {
@@ -3271,7 +3284,7 @@ var SleepManager = class {
3271
3284
  `dropping unparseable queued message ${row.id} for ${name}`,
3272
3285
  logger_default.errorData(err)
3273
3286
  );
3274
- await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
3287
+ await db.delete(deliveryQueue).where(eq2(deliveryQueue.id, row.id));
3275
3288
  continue;
3276
3289
  }
3277
3290
  const channel = payload.channel ?? "unknown";
@@ -3314,7 +3327,7 @@ var SleepManager = class {
3314
3327
  * the real delivery stack. Returns whether the batch was handed off to the mind.
3315
3328
  */
3316
3329
  async deliverQueuedBatch(name, payloads) {
3317
- const { deliverBatch: deliverBatch2 } = await import("./message-delivery-R7JSQGYX.js");
3330
+ const { deliverBatch: deliverBatch2 } = await import("./message-delivery-GPQ5SE5N.js");
3318
3331
  return deliverBatch2(name, payloads);
3319
3332
  }
3320
3333
  // --- Wake failure handling (#419) ---
@@ -3492,17 +3505,17 @@ var SleepManager = class {
3492
3505
  }
3493
3506
  }
3494
3507
  async waitForIdle(name, timeoutMs) {
3495
- return new Promise((resolve9) => {
3508
+ return new Promise((resolve10) => {
3496
3509
  const timeout = setTimeout(() => {
3497
3510
  unsub();
3498
- resolve9();
3511
+ resolve10();
3499
3512
  }, timeoutMs);
3500
3513
  const unsub = subscribe((event) => {
3501
3514
  if (event.mind !== name) return;
3502
3515
  if (event.type === "mind_done" || event.type === "mind_idle") {
3503
3516
  clearTimeout(timeout);
3504
3517
  unsub();
3505
- resolve9();
3518
+ resolve10();
3506
3519
  }
3507
3520
  });
3508
3521
  });
@@ -3510,15 +3523,15 @@ var SleepManager = class {
3510
3523
  async archiveSessions(name) {
3511
3524
  const dir = mindDir(name);
3512
3525
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
3513
- const sessionsDir = resolve7(dir, ".mind", "sessions");
3514
- if (existsSync8(sessionsDir)) {
3515
- const archiveDir = resolve7(sessionsDir, "archive");
3526
+ const sessionsDir = resolve8(dir, ".mind", "sessions");
3527
+ if (existsSync9(sessionsDir)) {
3528
+ const archiveDir = resolve8(sessionsDir, "archive");
3516
3529
  mkdirSync5(archiveDir, { recursive: true });
3517
3530
  for (const file of readdirSync2(sessionsDir)) {
3518
3531
  if (file === "archive" || !file.endsWith(".json")) continue;
3519
- const src = resolve7(sessionsDir, file);
3532
+ const src = resolve8(sessionsDir, file);
3520
3533
  const base = file.replace(/\.json$/, "");
3521
- const dest = resolve7(archiveDir, `${base}-${timestamp}.json`);
3534
+ const dest = resolve8(archiveDir, `${base}-${timestamp}.json`);
3522
3535
  try {
3523
3536
  renameSync(src, dest);
3524
3537
  } catch (err) {
@@ -3526,14 +3539,14 @@ var SleepManager = class {
3526
3539
  }
3527
3540
  }
3528
3541
  }
3529
- const piSessionsDir = resolve7(dir, ".mind", "pi-sessions");
3530
- if (existsSync8(piSessionsDir)) {
3531
- const archiveDir = resolve7(piSessionsDir, "archive");
3542
+ const piSessionsDir = resolve8(dir, ".mind", "pi-sessions");
3543
+ if (existsSync9(piSessionsDir)) {
3544
+ const archiveDir = resolve8(piSessionsDir, "archive");
3532
3545
  mkdirSync5(archiveDir, { recursive: true });
3533
3546
  for (const entry of readdirSync2(piSessionsDir, { withFileTypes: true })) {
3534
3547
  if (entry.name === "archive" || !entry.isDirectory()) continue;
3535
- const src = resolve7(piSessionsDir, entry.name);
3536
- const dest = resolve7(archiveDir, `${entry.name}-${timestamp}`);
3548
+ const src = resolve8(piSessionsDir, entry.name);
3549
+ const dest = resolve8(archiveDir, `${entry.name}-${timestamp}`);
3537
3550
  try {
3538
3551
  renameSync(src, dest);
3539
3552
  } catch (err) {
@@ -3543,8 +3556,8 @@ var SleepManager = class {
3543
3556
  }
3544
3557
  }
3545
3558
  async runWakeContextScript(name, sleepingSince, duration) {
3546
- const scriptPath = resolve7(mindDir(name), "home", ".local", "hooks", "wake-context.sh");
3547
- if (!existsSync8(scriptPath)) return "";
3559
+ const scriptPath = resolve8(mindDir(name), "home", ".local", "hooks", "wake-context.sh");
3560
+ if (!existsSync9(scriptPath)) return "";
3548
3561
  const input = JSON.stringify({
3549
3562
  sleepingSince,
3550
3563
  duration,
@@ -3594,7 +3607,7 @@ var SleepManager = class {
3594
3607
  async buildQueuedSummary(name) {
3595
3608
  try {
3596
3609
  const db = await getDb();
3597
- const rows = await db.select({ channel: deliveryQueue.channel, sender: deliveryQueue.sender }).from(deliveryQueue).where(and2(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
3610
+ 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();
3598
3611
  if (rows.length === 0) return "No messages arrived while you slept.";
3599
3612
  const channelCounts = /* @__PURE__ */ new Map();
3600
3613
  const senders = /* @__PURE__ */ new Set();
@@ -3641,7 +3654,7 @@ var SleepManager = class {
3641
3654
  } catch {
3642
3655
  try {
3643
3656
  const portHex = port.toString(16).toUpperCase().padStart(4, "0");
3644
- const tcp6 = readFileSync7("/proc/net/tcp6", "utf-8");
3657
+ const tcp6 = readFileSync8("/proc/net/tcp6", "utf-8");
3645
3658
  for (const line of tcp6.split("\n")) {
3646
3659
  if (!line.includes(`:${portHex} `)) continue;
3647
3660
  const fields = line.trim().split(/\s+/);
@@ -3709,9 +3722,9 @@ function getSleepManagerIfReady() {
3709
3722
  }
3710
3723
 
3711
3724
  // packages/daemon/src/lib/delivery/delivery-manager.ts
3712
- import { readFile, realpath } from "fs/promises";
3713
- import { extname, resolve as resolve8 } from "path";
3714
- import { and as and4, eq as eq5, inArray as inArray2, sql as sql2 } from "drizzle-orm";
3725
+ import { realpath } from "fs/promises";
3726
+ import { resolve as resolve9 } from "path";
3727
+ import { and as and3, eq as eq4, inArray as inArray2, sql } from "drizzle-orm";
3715
3728
 
3716
3729
  // packages/daemon/src/lib/chat/typing.ts
3717
3730
  var DEFAULT_TTL_MS = 1e4;
@@ -3805,7 +3818,7 @@ function publishTypingForChannels(channels, map) {
3805
3818
  }
3806
3819
 
3807
3820
  // packages/daemon/src/lib/delivery/send-gate.ts
3808
- import { and as and3, asc as asc2, desc, eq as eq4, gt } from "drizzle-orm";
3821
+ import { and as and2, asc, desc, eq as eq3, gt } from "drizzle-orm";
3809
3822
  var state = /* @__PURE__ */ new Map();
3810
3823
  function extractText(raw) {
3811
3824
  if (!raw) return "";
@@ -3820,7 +3833,7 @@ function extractText(raw) {
3820
3833
  }
3821
3834
  async function maxMessageId(conversationId) {
3822
3835
  const db = await getDb();
3823
- const rows = await db.select({ id: messages.id }).from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(desc(messages.id)).limit(1);
3836
+ const rows = await db.select({ id: messages.id }).from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(desc(messages.id)).limit(1);
3824
3837
  return rows[0]?.id ?? 0;
3825
3838
  }
3826
3839
  async function onDeliveredToMind(mindBase, conversationId) {
@@ -3839,7 +3852,7 @@ async function checkStaleSend(mindBase, conversationId, excludeSenders) {
3839
3852
  const entry = state.get(mindBase)?.get(conversationId);
3840
3853
  if (!entry?.turnOpen || entry.heldThisTurn) return { held: false };
3841
3854
  const db = await getDb();
3842
- const rows = await db.select({ id: messages.id, sender: messages.sender_name, content: messages.content }).from(messages).where(and3(eq4(messages.conversation_id, conversationId), gt(messages.id, entry.baselineId))).orderBy(asc2(messages.id));
3855
+ const rows = await db.select({ id: messages.id, sender: messages.sender_name, content: messages.content }).from(messages).where(and2(eq3(messages.conversation_id, conversationId), gt(messages.id, entry.baselineId))).orderBy(asc(messages.id));
3843
3856
  const excluded = new Set(excludeSenders.filter(Boolean));
3844
3857
  const unseen = rows.filter((r) => r.sender && !excluded.has(r.sender));
3845
3858
  if (unseen.length === 0) return { held: false };
@@ -3876,6 +3889,8 @@ var REDRIVE_INTERVAL_MS = 15e3;
3876
3889
  var RETRY_BASE_MS = 5e3;
3877
3890
  var RETRY_MAX_MS = 5 * 6e4;
3878
3891
  var REDRIVE_BATCH_LIMIT = 200;
3892
+ var GATED_RELEASE_LIMIT_PER_CHANNEL = 10;
3893
+ var GATED_NOTIFY_EVERY = 10;
3879
3894
  var mentionRegexCache = /* @__PURE__ */ new Map();
3880
3895
  var avatarBlocksCache = /* @__PURE__ */ new Map();
3881
3896
  var AVATAR_CACHE_TTL = 5 * 60 * 1e3;
@@ -3896,6 +3911,11 @@ var DeliveryManager = class {
3896
3911
  redriveTimer = null;
3897
3912
  /** Predicate for whether a mind is up; overridable in tests. */
3898
3913
  isMindRunning = (name) => tryGetMindManager()?.isRunning(name) ?? false;
3914
+ /** Sends a system message to a mind (invites, release summaries); overridable in tests. */
3915
+ notify = async (mindName, text) => {
3916
+ const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-63ABFF6V.js");
3917
+ await sendSystemMessage2(mindName, text);
3918
+ };
3899
3919
  constructor() {
3900
3920
  setRoutesChangeListener((mind) => {
3901
3921
  this.releaseGated(mind).catch(
@@ -3907,6 +3927,10 @@ var DeliveryManager = class {
3907
3927
  setRunningCheck(fn) {
3908
3928
  this.isMindRunning = fn;
3909
3929
  }
3930
+ /** Test seam: capture/override system notifications sent to minds. */
3931
+ setNotifier(fn) {
3932
+ this.notify = fn;
3933
+ }
3910
3934
  // --- Public API ---
3911
3935
  /**
3912
3936
  * Route and deliver a message to a mind. This is the main entry point.
@@ -4023,9 +4047,9 @@ var DeliveryManager = class {
4023
4047
  try {
4024
4048
  const db = await getDb();
4025
4049
  rows = await db.select().from(deliveryQueue).where(
4026
- and4(
4027
- eq5(deliveryQueue.status, "pending"),
4028
- sql2`(${deliveryQueue.next_attempt_at} IS NULL OR ${deliveryQueue.next_attempt_at} <= datetime('now'))`
4050
+ and3(
4051
+ eq4(deliveryQueue.status, "pending"),
4052
+ sql`(${deliveryQueue.next_attempt_at} IS NULL OR ${deliveryQueue.next_attempt_at} <= datetime('now'))`
4029
4053
  )
4030
4054
  ).orderBy(deliveryQueue.id).limit(REDRIVE_BATCH_LIMIT);
4031
4055
  } catch (err) {
@@ -4069,9 +4093,15 @@ var DeliveryManager = class {
4069
4093
  if (redriven > 0) dlog.info(`redrove ${redriven} pending delivery queue rows`);
4070
4094
  }
4071
4095
  /**
4072
- * Re-evaluate a mind's `gated` rows against its current routes.json and promote any
4073
- * that now match to `pending` (delivered by the next redrive sweep). Called when the
4074
- * routing config changes.
4096
+ * Re-evaluate a mind's `gated` rows against its current routes.json when the routing
4097
+ * config changes. For each channel that now matches a `mind` route:
4098
+ * - the newest {@link GATED_RELEASE_LIMIT_PER_CHANNEL} rows are promoted to `pending`
4099
+ * and re-stamped with the freshly-resolved session (NOT the gate-time fallback), so
4100
+ * they land in the correct session instead of `main` (#537 bug 1);
4101
+ * - any older rows are `archived` (inert) so a long backlog can't flood the mind
4102
+ * in one sweep (#537 bug 2), and the mind gets one summary rather than a flood.
4103
+ * Rows resolving to a `file` destination are archived (the delivery manager doesn't
4104
+ * deliver file routes). Declined channels are skipped entirely — they stay gated.
4075
4105
  */
4076
4106
  async releaseGated(mindName) {
4077
4107
  const baseName = await getBaseName(mindName);
@@ -4079,12 +4109,13 @@ var DeliveryManager = class {
4079
4109
  let rows;
4080
4110
  try {
4081
4111
  const db = await getDb();
4082
- rows = await db.select().from(deliveryQueue).where(and4(eq5(deliveryQueue.mind, baseName), eq5(deliveryQueue.status, "gated")));
4112
+ rows = await db.select().from(deliveryQueue).where(and3(eq4(deliveryQueue.mind, baseName), eq4(deliveryQueue.status, "gated")));
4083
4113
  } catch (err) {
4084
4114
  dlog.warn(`failed to read gated rows for ${baseName}`, logger_default.errorData(err));
4085
4115
  return;
4086
4116
  }
4087
- const promote = [];
4117
+ const byChannel = /* @__PURE__ */ new Map();
4118
+ const archiveIds = [];
4088
4119
  for (const row of rows) {
4089
4120
  let payload;
4090
4121
  try {
@@ -4099,27 +4130,127 @@ var DeliveryManager = class {
4099
4130
  participantCount: payload.participantCount
4100
4131
  };
4101
4132
  const route = resolveRoute(config, meta);
4102
- if (route.matched && route.destination === "mind") {
4103
- promote.push(row.id);
4133
+ if (!route.matched) continue;
4134
+ if (route.destination === "file") {
4135
+ archiveIds.push(row.id);
4136
+ continue;
4137
+ }
4138
+ let session = route.session;
4139
+ if (session === "$new") {
4140
+ session = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4141
+ }
4142
+ const channel = row.channel ?? payload.channel ?? "unknown";
4143
+ const list = byChannel.get(channel) ?? [];
4144
+ list.push({ id: row.id, session });
4145
+ byChannel.set(channel, list);
4146
+ }
4147
+ const promote = [];
4148
+ const truncationNotes = [];
4149
+ for (const [channel, items] of byChannel) {
4150
+ if (await this.isChannelDeclined(baseName, channel)) continue;
4151
+ items.sort((a, b) => b.id - a.id);
4152
+ const keep = items.slice(0, GATED_RELEASE_LIMIT_PER_CHANNEL);
4153
+ const drop = items.slice(GATED_RELEASE_LIMIT_PER_CHANNEL);
4154
+ promote.push(...keep);
4155
+ for (const d of drop) archiveIds.push(d.id);
4156
+ if (drop.length > 0) {
4157
+ truncationNotes.push(
4158
+ `${channel}: released the ${keep.length} most recent message(s); ${drop.length} earlier message(s) were held while unrouted and remain in the channel history (volute chat read ${channel}).`
4159
+ );
4160
+ dlog.info(
4161
+ `truncated gated release for ${baseName} on ${channel}: kept ${keep.length}, archived ${drop.length}`
4162
+ );
4163
+ }
4164
+ }
4165
+ if (archiveIds.length > 0) {
4166
+ try {
4167
+ const db = await getDb();
4168
+ for (let i = 0; i < archiveIds.length; i += 500) {
4169
+ await db.update(deliveryQueue).set({ status: "archived" }).where(inArray2(deliveryQueue.id, archiveIds.slice(i, i + 500)));
4170
+ }
4171
+ } catch (err) {
4172
+ dlog.warn(`failed to archive gated rows for ${baseName}`, logger_default.errorData(err));
4104
4173
  }
4105
4174
  }
4106
- if (promote.length === 0) return;
4175
+ if (promote.length === 0) {
4176
+ if (truncationNotes.length > 0) await this.sendReleaseSummary(mindName, truncationNotes);
4177
+ return;
4178
+ }
4107
4179
  try {
4108
4180
  const db = await getDb();
4109
- await db.update(deliveryQueue).set({ status: "pending", attempts: 0, next_attempt_at: null }).where(inArray2(deliveryQueue.id, promote));
4181
+ for (const p of promote) {
4182
+ await db.update(deliveryQueue).set({ status: "pending", session: p.session, attempts: 0, next_attempt_at: null }).where(eq4(deliveryQueue.id, p.id));
4183
+ }
4110
4184
  dlog.info(`released ${promote.length} gated message(s) for ${baseName} after route change`);
4111
4185
  } catch (err) {
4112
4186
  dlog.warn(`failed to promote gated rows for ${baseName}`, logger_default.errorData(err));
4113
4187
  return;
4114
4188
  }
4189
+ if (truncationNotes.length > 0) await this.sendReleaseSummary(mindName, truncationNotes);
4115
4190
  await this.redrive();
4116
4191
  }
4192
+ /**
4193
+ * Whether the mind has explicitly declined a channel. A declined channel keeps
4194
+ * persisting history but never notifies and is never released. #537
4195
+ */
4196
+ async isChannelDeclined(baseName, channel) {
4197
+ if (!channel) return false;
4198
+ try {
4199
+ const db = await getDb();
4200
+ const rows = await db.select({ state: channelGates.state }).from(channelGates).where(and3(eq4(channelGates.mind, baseName), eq4(channelGates.channel, channel)));
4201
+ return rows[0]?.state === "declined";
4202
+ } catch (err) {
4203
+ dlog.warn(`failed to read gate state for ${baseName}/${channel}`, logger_default.errorData(err));
4204
+ return false;
4205
+ }
4206
+ }
4207
+ /**
4208
+ * Record that a mind has declined an unrouted channel: future messages are still
4209
+ * persisted (history is preserved) but never notify, and any currently-gated rows are
4210
+ * archived so they're inert. Returns the number of held messages archived. #537
4211
+ */
4212
+ async declineChannel(mindName, channel) {
4213
+ const baseName = await getBaseName(mindName);
4214
+ const db = await getDb();
4215
+ await db.insert(channelGates).values({ mind: baseName, channel, state: "declined" }).onConflictDoUpdate({
4216
+ target: [channelGates.mind, channelGates.channel],
4217
+ set: { state: "declined", updated_at: sql`(datetime('now'))` }
4218
+ });
4219
+ const archived = await db.update(deliveryQueue).set({ status: "archived" }).where(
4220
+ and3(
4221
+ eq4(deliveryQueue.mind, baseName),
4222
+ eq4(deliveryQueue.channel, channel),
4223
+ eq4(deliveryQueue.status, "gated")
4224
+ )
4225
+ ).returning({ id: deliveryQueue.id });
4226
+ dlog.info(
4227
+ `declined channel ${channel} for ${baseName}; archived ${archived.length} held row(s)`
4228
+ );
4229
+ return archived.length;
4230
+ }
4231
+ /**
4232
+ * Send the mind a single summary when a routing change released a truncated backlog,
4233
+ * rather than a flood of individual messages. #537
4234
+ */
4235
+ async sendReleaseSummary(mindName, notes) {
4236
+ const body = [
4237
+ `[Channel backlog released]`,
4238
+ `A routing change matched channel(s) that had held messages while unrouted. To avoid flooding you, only the ${GATED_RELEASE_LIMIT_PER_CHANNEL} most recent per channel were delivered:`,
4239
+ "",
4240
+ ...notes.map((n) => `- ${n}`)
4241
+ ].join("\n");
4242
+ try {
4243
+ await this.notify(mindName, body);
4244
+ } catch (err) {
4245
+ dlog.warn(`failed to send release summary for ${mindName}`, logger_default.errorData(err));
4246
+ }
4247
+ }
4117
4248
  /**
4118
4249
  * Get pending (gated) messages for a mind.
4119
4250
  */
4120
4251
  async getPending(mindName) {
4121
4252
  const db = await getDb();
4122
- const rows = await db.select().from(deliveryQueue).where(and4(eq5(deliveryQueue.mind, mindName), eq5(deliveryQueue.status, "gated")));
4253
+ const rows = await db.select().from(deliveryQueue).where(and3(eq4(deliveryQueue.mind, mindName), eq4(deliveryQueue.status, "gated")));
4123
4254
  const byChannel = /* @__PURE__ */ new Map();
4124
4255
  for (const row of rows) {
4125
4256
  const ch = row.channel ?? "unknown";
@@ -4289,7 +4420,7 @@ var DeliveryManager = class {
4289
4420
  const backoffSec = Math.round(
4290
4421
  Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** Math.min(attempts, 20)) / 1e3
4291
4422
  );
4292
- await db.update(deliveryQueue).set({ attempts, next_attempt_at: sql2`datetime('now', ${`+${backoffSec} seconds`})` }).where(eq5(deliveryQueue.id, row.id));
4423
+ await db.update(deliveryQueue).set({ attempts, next_attempt_at: sql`datetime('now', ${`+${backoffSec} seconds`})` }).where(eq4(deliveryQueue.id, row.id));
4293
4424
  }
4294
4425
  } catch (err) {
4295
4426
  dlog.warn("failed to schedule delivery retry", logger_default.errorData(err));
@@ -4513,41 +4644,44 @@ var DeliveryManager = class {
4513
4644
  }
4514
4645
  async gateMessage(mindName, session, payload) {
4515
4646
  const baseName = await getBaseName(mindName);
4516
- await this.persistToQueue(mindName, session, payload, "gated");
4647
+ const declined = await this.isChannelDeclined(baseName, payload.channel);
4648
+ await this.persistToQueue(mindName, session, payload, declined ? "archived" : "gated");
4649
+ if (declined) return;
4517
4650
  try {
4518
4651
  const db = await getDb();
4519
- const count = await db.select({ count: sql2`count(*)` }).from(deliveryQueue).where(
4520
- and4(
4521
- eq5(deliveryQueue.mind, baseName),
4522
- eq5(deliveryQueue.channel, payload.channel),
4523
- eq5(deliveryQueue.status, "gated")
4652
+ const rows = await db.select({ count: sql`count(*)` }).from(deliveryQueue).where(
4653
+ and3(
4654
+ eq4(deliveryQueue.mind, baseName),
4655
+ eq4(deliveryQueue.channel, payload.channel),
4656
+ inArray2(deliveryQueue.status, ["gated", "archived"])
4524
4657
  )
4525
4658
  );
4526
- if ((count[0]?.count ?? 0) <= 1) {
4527
- await this.sendInviteNotification(mindName, payload);
4659
+ const count = rows[0]?.count ?? 0;
4660
+ if (count === 1 || count % GATED_NOTIFY_EVERY === 0) {
4661
+ await this.sendInviteNotification(mindName, payload, count);
4528
4662
  }
4529
4663
  } catch (err) {
4530
4664
  dlog.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
4531
4665
  }
4532
4666
  }
4533
- async sendInviteNotification(mindName, payload) {
4667
+ async sendInviteNotification(mindName, payload, gatedCount = 1) {
4534
4668
  const text = extractTextContent(payload.content);
4535
4669
  const preview = text.length > 200 ? `${text.slice(0, 200)}...` : text;
4536
4670
  const channel = payload.channel ?? "unknown";
4671
+ const heldLine = gatedCount > 1 ? `${gatedCount} messages from this channel are being held, unrouted \u2014 you've not routed it yet.` : `Someone new is reaching out \u2014 you don't have a route for this channel yet.`;
4537
4672
  const notification = [
4538
4673
  `[New channel: ${channel}]`,
4539
- `Someone new is reaching out \u2014 you don't have a route for this channel yet.`,
4674
+ heldLine,
4540
4675
  `Sender: ${payload.sender ?? "unknown"}`,
4541
4676
  payload.platform ? `Platform: ${payload.platform}` : null,
4542
4677
  payload.participantCount ? `Participants: ${payload.participantCount}` : null,
4543
4678
  "",
4544
4679
  `Preview: ${preview}`,
4545
4680
  "",
4546
- `To start hearing this channel, add a routing rule for "${channel}" to .config/routes.json.`,
4547
- `Messages are held safely until you do \u2014 nothing is lost. If you'd rather not engage, just leave it unrouted.`
4681
+ `To start hearing this channel, add a routing rule for "${channel}" to .config/routes.json \u2014 only the ${GATED_RELEASE_LIMIT_PER_CHANNEL} most recent held messages are replayed; older ones stay in the channel history (volute chat read ${channel}).`,
4682
+ `To stop hearing about it, decline the channel: volute chat channels decline ${channel}`
4548
4683
  ].filter((line) => line !== null).join("\n");
4549
- const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-NDAW6YP7.js");
4550
- await sendSystemMessage2(mindName, notification);
4684
+ await this.notify(mindName, notification);
4551
4685
  }
4552
4686
  /**
4553
4687
  * Insert a delivery_queue row and return its id. The `mind` column is always keyed by
@@ -4612,18 +4746,6 @@ var DeliveryManager = class {
4612
4746
  const cached = avatarBlocksCache.get(cacheKey);
4613
4747
  if (cached && cached.expiresAt > Date.now()) return cached.blocks;
4614
4748
  const blocks = [];
4615
- let sharpDefault = null;
4616
- try {
4617
- const mod = await import("sharp");
4618
- sharpDefault = mod.default ?? mod;
4619
- } catch (err) {
4620
- const code = err.code;
4621
- if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
4622
- dlog.debug("sharp not available, sending full-size avatars");
4623
- } else {
4624
- dlog.warn("sharp import failed, sending full-size avatars", logger_default.errorData(err));
4625
- }
4626
- }
4627
4749
  for (const p of participants) {
4628
4750
  if (!p.avatar) continue;
4629
4751
  try {
@@ -4632,8 +4754,8 @@ var DeliveryManager = class {
4632
4754
  const dir = mindDir(p.username);
4633
4755
  const config = readVoluteConfig(dir);
4634
4756
  if (!config?.profile?.avatar) continue;
4635
- filePath = resolve8(dir, "home", config.profile.avatar);
4636
- const homeDir = resolve8(dir, "home");
4757
+ filePath = resolve9(dir, "home", config.profile.avatar);
4758
+ const homeDir = resolve9(dir, "home");
4637
4759
  if (!filePath.startsWith(`${homeDir}/`)) {
4638
4760
  dlog.warn(`avatar path for ${p.username} escapes home directory, skipping`);
4639
4761
  continue;
@@ -4652,34 +4774,10 @@ var DeliveryManager = class {
4652
4774
  throw err;
4653
4775
  }
4654
4776
  } else {
4655
- filePath = resolve8(voluteHome(), "avatars", p.avatar);
4656
- }
4657
- const ext = extname(filePath).toLowerCase();
4658
- const mimeMap = {
4659
- ".png": "image/png",
4660
- ".jpg": "image/jpeg",
4661
- ".jpeg": "image/jpeg",
4662
- ".gif": "image/gif",
4663
- ".webp": "image/webp"
4664
- };
4665
- const mediaType = mimeMap[ext];
4666
- if (!mediaType) continue;
4667
- const data = await readFile(filePath);
4668
- let imageData = data;
4669
- if (sharpDefault) {
4670
- try {
4671
- imageData = await sharpDefault(data).resize(128, 128, { fit: "cover" }).toBuffer();
4672
- } catch (err) {
4673
- dlog.warn(
4674
- `avatar resize failed for ${p.username}, sending original`,
4675
- logger_default.errorData(err)
4676
- );
4677
- }
4777
+ filePath = resolve9(voluteHome(), "avatars", p.avatar);
4678
4778
  }
4679
- blocks.push(
4680
- { type: "text", text: `[Avatar for ${p.username}]` },
4681
- { type: "image", media_type: mediaType, data: imageData.toString("base64") }
4682
- );
4779
+ const rendered = await renderAvatarBlock(filePath, p.username);
4780
+ if (rendered) blocks.push(...rendered);
4683
4781
  } catch (err) {
4684
4782
  const code = err.code;
4685
4783
  if (code !== "ENOENT") {
@@ -4804,7 +4902,7 @@ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEven
4804
4902
  content: mindHistory.content,
4805
4903
  message_id: mindHistory.message_id,
4806
4904
  turn_id: mindHistory.turn_id
4807
- }).from(mindHistory).where(and5(eq6(mindHistory.id, outboundId), eq6(mindHistory.mind, mind))).limit(1);
4905
+ }).from(mindHistory).where(and4(eq5(mindHistory.id, outboundId), eq5(mindHistory.mind, mind))).limit(1);
4808
4906
  const row = rows[0];
4809
4907
  if (!row) {
4810
4908
  dlog2.warn(`outbound marker references missing record: mind=${mind} id=${outboundId}`);
@@ -4812,13 +4910,13 @@ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEven
4812
4910
  }
4813
4911
  const alreadyTagged = row.turn_id != null;
4814
4912
  if (!alreadyTagged) {
4815
- await db.update(mindHistory).set({ turn_id: turnId }).where(eq6(mindHistory.id, outboundId));
4913
+ await db.update(mindHistory).set({ turn_id: turnId }).where(eq5(mindHistory.id, outboundId));
4816
4914
  }
4817
4915
  if (row.message_id) {
4818
4916
  await db.update(messages).set({
4819
4917
  turn_id: turnId,
4820
4918
  ...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
4821
- }).where(eq6(messages.id, Number(row.message_id)));
4919
+ }).where(eq5(messages.id, Number(row.message_id)));
4822
4920
  }
4823
4921
  if (!alreadyTagged) {
4824
4922
  publish3(mind, {
@@ -5007,7 +5105,7 @@ async function spiritWillHandle() {
5007
5105
  const spiritEntry = await findMind(SPIRIT_NAME);
5008
5106
  if (spiritEntry?.running && spiritEntry.mindType === "spirit") return true;
5009
5107
  try {
5010
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
5108
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-Z27R3WTW.js");
5011
5109
  const sm = getSleepManagerIfReady2();
5012
5110
  if (sm?.isSleeping(SPIRIT_NAME)) return true;
5013
5111
  } catch (err) {
@@ -5035,7 +5133,7 @@ async function generateSystemFallbackReply(conversationId, mindName, message) {
5035
5133
  if (config.sleep.schedule?.wake) contextParts.push(`Wake cron: ${config.sleep.schedule.wake}`);
5036
5134
  }
5037
5135
  try {
5038
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
5136
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-Z27R3WTW.js");
5039
5137
  const sm = getSleepManagerIfReady2();
5040
5138
  if (sm) {
5041
5139
  const state2 = sm.getState(mindName);
@@ -5089,10 +5187,6 @@ export {
5089
5187
  checkHealth,
5090
5188
  RotatingLog,
5091
5189
  syncProviderToMinds,
5092
- recordNotice,
5093
- drainNotices,
5094
- clearDeliveredNotices,
5095
- formatNotices,
5096
5190
  RestartTracker,
5097
5191
  getTypingMap,
5098
5192
  isConversationId,
@@ -5120,6 +5214,7 @@ export {
5120
5214
  readSystemsConfig,
5121
5215
  writeSystemsConfig,
5122
5216
  deleteSystemsConfig,
5217
+ enrichActivityMetadata,
5123
5218
  parseCommandArgs,
5124
5219
  loadAllExtensions,
5125
5220
  getLoadedExtensions,
@@ -5140,6 +5235,7 @@ export {
5140
5235
  initTokenBudget,
5141
5236
  getTokenBudget,
5142
5237
  startMindFull,
5238
+ recordMissingCredentialsNotice,
5143
5239
  sleepMind,
5144
5240
  wakeMind,
5145
5241
  startSpiritFull,
@@ -5159,5 +5255,6 @@ export {
5159
5255
  deliverBatch,
5160
5256
  ensureSystemChannel,
5161
5257
  joinSystemChannel,
5258
+ backfillSystemChannelMembers,
5162
5259
  announceToSystem
5163
5260
  };