volute 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +7 -6
  2. package/dist/accept-ZBDVVCEU.js +42 -0
  3. package/dist/activity-events-ZW4SDL2C.js +15 -0
  4. package/dist/{ai-service-SBY2WG7O.js → ai-service-LURBEDDB.js} +6 -6
  5. package/dist/{api-client-YPKOZP2O.js → api-client-3A77HMH7.js} +2 -2
  6. package/dist/api.d.ts +1 -5195
  7. package/dist/{archive-INXYFVCW.js → archive-ESU2FUN4.js} +4 -4
  8. package/dist/{auth-GKCDSO4T.js → auth-WX4TESEI.js} +6 -6
  9. package/dist/bridge-PXIO6PS2.js +206 -0
  10. package/dist/chat-QXAJF3FU.js +51 -0
  11. package/dist/{chunk-NNB4WIG7.js → chunk-2TGZJFAT.js} +3 -3
  12. package/dist/{chunk-6LXAAQ43.js → chunk-33ODGMFZ.js} +1 -1
  13. package/dist/{chunk-RPZZSXV3.js → chunk-5N7Y5WAM.js} +21 -2
  14. package/dist/chunk-5T5YMX6S.js +23 -0
  15. package/dist/{chunk-7J3HEVR7.js → chunk-5XJYUFZH.js} +28 -16
  16. package/dist/chunk-7KJOFUNN.js +22 -0
  17. package/dist/{chunk-2NGTS5UU.js → chunk-A2ZLHBHG.js} +2 -2
  18. package/dist/{chunk-KIEPMIM5.js → chunk-AN2W47GW.js} +2 -2
  19. package/dist/{chunk-G53F3JA4.js → chunk-AOB6GVRM.js} +1 -1
  20. package/dist/{chunk-LRCG2JLP.js → chunk-BDYXIWA5.js} +9 -5
  21. package/dist/{chunk-YUIHSKR6.js → chunk-BKF4WQCY.js} +2 -2
  22. package/dist/{chunk-N432I7QH.js → chunk-BMZQYACC.js} +2 -2
  23. package/dist/{chunk-NAOW2CLO.js → chunk-BTY4WNFE.js} +1 -1
  24. package/dist/{chunk-ALEF47VT.js → chunk-BV65KRHM.js} +2 -2
  25. package/dist/{chunk-KVK2DLWI.js → chunk-CORXD635.js} +4 -4
  26. package/dist/{chunk-PVY5W6QN.js → chunk-F7ZNLYKZ.js} +2 -2
  27. package/dist/{chunk-QTUVYI7W.js → chunk-FT5KETXZ.js} +3 -3
  28. package/dist/{chunk-C7I35G4R.js → chunk-IJHIXLVN.js} +44 -8
  29. package/dist/{chunk-JUKK7FPS.js → chunk-J6CJQDWI.js} +37 -28
  30. package/dist/{chunk-4RQBJWQX.js → chunk-LOPXTW6H.js} +1 -1
  31. package/dist/{chunk-RSX4OPZY.js → chunk-MDJGMOSD.js} +8 -137
  32. package/dist/{chunk-LOEJ4HPQ.js → chunk-N446KRP7.js} +3 -3
  33. package/dist/{chunk-I5KY25PQ.js → chunk-N5LMGYXX.js} +2 -2
  34. package/dist/{chunk-G6BSYHPK.js → chunk-NJK5SDGR.js} +1 -1
  35. package/dist/{chunk-D424ZQGI.js → chunk-O7IGP7ZW.js} +11 -3
  36. package/dist/{chunk-M7UL5S3Q.js → chunk-OTC67N2Z.js} +2 -2
  37. package/dist/{chunk-GY5HBI7A.js → chunk-PWQ2ITYG.js} +4 -4
  38. package/dist/{chunk-KTLFDYPT.js → chunk-QCH6K235.js} +1 -1
  39. package/dist/chunk-QHG4OMZL.js +145 -0
  40. package/dist/{chunk-SKLSMHXO.js → chunk-QWTR6AWZ.js} +3 -3
  41. package/dist/chunk-TXSA4Q3V.js +116 -0
  42. package/dist/{chunk-VH33ZWMW.js → chunk-VHJRZM2S.js} +2 -2
  43. package/dist/{chunk-SSI47XP2.js → chunk-VHWGEJ4V.js} +1 -1
  44. package/dist/chunk-VY3RB2V7.js +164 -0
  45. package/dist/chunk-WJPROOU5.js +8314 -0
  46. package/dist/{chunk-RVGLDGMI.js → chunk-WZRZFFCL.js} +25 -27
  47. package/dist/{chunk-JYVGHWEJ.js → chunk-XRQSAMX2.js} +4 -4
  48. package/dist/{chunk-OYAKCAVY.js → chunk-ZSR72JB3.js} +1 -1
  49. package/dist/{chunk-UKVWJRKN.js → chunk-ZX7EAV5J.js} +17 -7
  50. package/dist/cli.js +90 -29
  51. package/dist/clock-HSEKS5AR.js +289 -0
  52. package/dist/{cloud-sync-4NWLMFVH.js → cloud-sync-6JL4C24T.js} +22 -23
  53. package/dist/config-UTS7QULS.js +76 -0
  54. package/dist/connectors/discord-bridge.js +4 -4
  55. package/dist/connectors/slack-bridge.js +4 -4
  56. package/dist/connectors/telegram-bridge.js +4 -4
  57. package/dist/{conversations-AWI5SZW2.js → conversations-2PW57WO2.js} +6 -6
  58. package/dist/create-5BPOOJAN.js +75 -0
  59. package/dist/create-UVCK2CS6.js +50 -0
  60. package/dist/daemon-client-RVIKXGFQ.js +12 -0
  61. package/dist/daemon-restart-HSZ3BCX5.js +65 -0
  62. package/dist/daemon.js +1349 -1211
  63. package/dist/db-BDMH4SZ2.js +20 -0
  64. package/dist/db-BVBJ57TU.js +9 -0
  65. package/dist/delete-L5PAVDGQ.js +42 -0
  66. package/dist/delivery-manager-H5ZVBMCQ.js +31 -0
  67. package/dist/{delivery-router-FL45JL7N.js → delivery-router-HEJSJAHQ.js} +5 -5
  68. package/dist/down-74VXM45A.js +17 -0
  69. package/dist/env-E4XHO2BI.js +223 -0
  70. package/dist/exec-PY7THYH4.js +17 -0
  71. package/dist/export-OAS6QVBN.js +113 -0
  72. package/dist/extension-D74CNM7G.js +89 -0
  73. package/dist/extensions-XDDFY72A.js +49 -0
  74. package/dist/files-CWTK6V3H.js +53 -0
  75. package/dist/import-5A3T7QV4.js +143 -0
  76. package/dist/{isolation-LLAYQYDY.js → isolation-TK5RX2WM.js} +4 -4
  77. package/dist/join-DF5XSJAC.js +67 -0
  78. package/dist/lib-DYEZMGW7.js +6588 -0
  79. package/dist/list-PDMQM7ZV.js +53 -0
  80. package/dist/login-7TE6CIZF.js +60 -0
  81. package/dist/login-GOTAYLXP.js +51 -0
  82. package/dist/logout-6KIA74EV.js +29 -0
  83. package/dist/logout-T4XS6LRU.js +50 -0
  84. package/dist/message-delivery-GRC4W6P7.js +41 -0
  85. package/dist/mind-5IEYKV7I.js +97 -0
  86. package/dist/mind-activity-tracker-QBLIV7ZJ.js +18 -0
  87. package/dist/mind-history-IE2QH7U5.js +275 -0
  88. package/dist/mind-list-GEWHWAL4.js +38 -0
  89. package/dist/mind-manager-HFLB5653.js +31 -0
  90. package/dist/mind-profile-DCBDVF5B.js +53 -0
  91. package/dist/mind-service-X2CAA6W6.js +37 -0
  92. package/dist/mind-sleep-ITCF6OQA.js +47 -0
  93. package/dist/mind-status-X4SX3YUG.js +65 -0
  94. package/dist/mind-wake-KXMKMGWX.js +42 -0
  95. package/dist/{package-U3VFO273.js → package-D2FSVFAX.js} +11 -8
  96. package/dist/read-67VRP2DO.js +91 -0
  97. package/dist/{read-stdin-HQJ7774D.js → read-stdin-3X5VYKNS.js} +2 -2
  98. package/dist/register-SB7NXCOE.js +51 -0
  99. package/dist/{registry-PJ4S5PHQ.js → registry-GBSNW3HG.js} +3 -3
  100. package/dist/reject-MUR2KWJ4.js +40 -0
  101. package/dist/restart-5EGG4JXU.js +42 -0
  102. package/dist/{sandbox-GJOK4QLQ.js → sandbox-R37VIU36.js} +6 -6
  103. package/dist/scheduler-Y7O4CJXL.js +31 -0
  104. package/dist/{schema-PA3M5ZKH.js → schema-XVZ2CLKW.js} +4 -2
  105. package/dist/{seed-QDYVLG74.js → seed-EQORWX77.js} +3 -3
  106. package/dist/seed-check-KJNTL72M.js +35 -0
  107. package/dist/seed-cmd-ZM2XGVU2.js +30 -0
  108. package/dist/seed-create-DRWGGHEI.js +113 -0
  109. package/dist/seed-sprout-JYXGXOP3.js +148 -0
  110. package/dist/send-JBJJQ7CA.js +409 -0
  111. package/dist/service-WNPCNHOX.js +121 -0
  112. package/dist/{setup-XMCBE3LF.js → setup-BJ4YAY26.js} +155 -129
  113. package/dist/{setup-TISPCO22.js → setup-RHJRFURI.js} +4 -4
  114. package/dist/skill-TAAKEYBV.js +389 -0
  115. package/dist/skills/plan-coordinator/SKILL.md +60 -0
  116. package/dist/skills/volute-mind/SKILL.md +9 -227
  117. package/dist/skills/volute-mind/references/extensions.md +34 -0
  118. package/dist/skills/volute-mind/references/integrations.md +48 -0
  119. package/dist/skills/volute-mind/references/routing.md +86 -0
  120. package/dist/skills/volute-mind/references/sleep.md +33 -0
  121. package/dist/skills/volute-mind/references/variants.md +31 -0
  122. package/dist/{skills-7FV7EJTE.js → skills-EKMCQ46K.js} +12 -8
  123. package/dist/sleep-manager-7KFK3USC.js +35 -0
  124. package/dist/spirit-ZFRDXMG7.js +23 -0
  125. package/dist/split-AWVOYOPZ.js +64 -0
  126. package/dist/{sprout-WKLZXUIQ.js → sprout-HE4TITMK.js} +3 -3
  127. package/dist/start-3UXOPXQG.js +39 -0
  128. package/dist/status-ZK34WYIM.js +125 -0
  129. package/dist/stop-3XYIBGFM.js +41 -0
  130. package/dist/system-chat-IDPHYHY4.js +35 -0
  131. package/dist/systems-O43WGQY6.js +52 -0
  132. package/dist/{tailscale-XHQBZROW.js → tailscale-ZIZ2HWJ5.js} +5 -5
  133. package/dist/template-hash-A7FNHTB7.js +9 -0
  134. package/dist/up-77ICEDEW.js +19 -0
  135. package/dist/update-ANE5ZM7F.js +225 -0
  136. package/dist/{update-check-ZD6OOIYQ.js → update-check-UV55CBEP.js} +4 -4
  137. package/dist/upgrade-ZMDGC7M2.js +74 -0
  138. package/dist/variant-QWL2WSRI.js +62 -0
  139. package/dist/{version-notify-NBI2MTJO.js → version-notify-FXSEMXWW.js} +29 -28
  140. package/dist/{volute-config-HD7WWUQC.js → volute-config-D2XVS2YI.js} +2 -2
  141. package/dist/web-assets/assets/index-BhxWKvbB.css +1 -0
  142. package/dist/web-assets/assets/index-CHVKJ9II.js +75 -0
  143. package/dist/web-assets/ext-theme.css +48 -9
  144. package/dist/web-assets/index.html +2 -2
  145. package/dist/web-assets/sw.js +117 -0
  146. package/drizzle/0005_meta_summaries.sql +15 -0
  147. package/drizzle/meta/0005_snapshot.json +7 -0
  148. package/drizzle/meta/_journal.json +7 -0
  149. package/package.json +10 -7
  150. package/packages/extensions/pages/dist/ui/assets/index-DKZLNMED.js +2 -0
  151. package/packages/extensions/pages/dist/ui/index.html +1 -1
  152. package/packages/extensions/pages/skills/pages/SKILL.md +84 -9
  153. package/packages/extensions/plan/dist/ui/assets/index-CJj2gZnZ.css +1 -0
  154. package/packages/extensions/plan/dist/ui/assets/index-FMEJmvQz.js +61 -0
  155. package/packages/extensions/plan/dist/ui/index.html +14 -0
  156. package/packages/extensions/plan/skills/plan/SKILL.md +43 -0
  157. package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +37 -0
  158. package/templates/_base/home/VOLUTE.md +12 -19
  159. package/templates/_base/src/lib/auto-commit.ts +8 -8
  160. package/templates/_base/src/lib/context-breakdown.ts +450 -0
  161. package/templates/_base/src/lib/format-prefix.ts +17 -0
  162. package/templates/_base/src/lib/hook-loader.ts +8 -2
  163. package/templates/_base/src/lib/router.ts +75 -33
  164. package/templates/_base/src/lib/routing.ts +4 -1
  165. package/templates/_base/src/lib/startup.ts +16 -8
  166. package/templates/_base/src/lib/types.ts +2 -1
  167. package/templates/_base/src/lib/volute-server.ts +75 -8
  168. package/templates/claude/.init/CLAUDE.md +4 -10
  169. package/templates/claude/package.json.tmpl +1 -0
  170. package/templates/claude/src/agent.ts +108 -33
  171. package/templates/claude/src/lib/hooks/reply-instructions.ts +27 -7
  172. package/templates/claude/src/lib/stream-consumer.ts +2 -2
  173. package/templates/claude/src/server.ts +1 -0
  174. package/templates/codex/package.json.tmpl +1 -0
  175. package/templates/codex/src/agent.ts +80 -8
  176. package/templates/codex/src/server.ts +1 -4
  177. package/templates/pi/package.json.tmpl +1 -0
  178. package/templates/pi/src/agent.ts +115 -36
  179. package/templates/pi/src/lib/event-handler.ts +22 -7
  180. package/templates/pi/src/lib/reply-instructions-extension.ts +23 -4
  181. package/templates/pi/src/lib/subagents.ts +20 -17
  182. package/templates/pi/src/server.ts +2 -5
  183. package/dist/accept-D5VBM7JW.js +0 -42
  184. package/dist/activity-events-XJO3P4RR.js +0 -15
  185. package/dist/bridge-TXWWPPOJ.js +0 -207
  186. package/dist/chat-U5ZOME3O.js +0 -68
  187. package/dist/chunk-3Z2DPESO.js +0 -3634
  188. package/dist/chunk-A2A4KLFE.js +0 -1528
  189. package/dist/chunk-K3NQKI34.js +0 -10
  190. package/dist/chunk-NPKSDYA2.js +0 -156
  191. package/dist/chunk-PB65JZK2.js +0 -85
  192. package/dist/clock-BVH3V6E3.js +0 -266
  193. package/dist/config-H2H4UIF7.js +0 -72
  194. package/dist/create-2FK7Z46Y.js +0 -44
  195. package/dist/create-YWD2TIP4.js +0 -71
  196. package/dist/daemon-client-6QXHZ7US.js +0 -12
  197. package/dist/daemon-restart-GOBUKLX7.js +0 -52
  198. package/dist/db-F34YLV7D.js +0 -9
  199. package/dist/db-RA45JBFG.js +0 -16
  200. package/dist/delete-QTGWEDBI.js +0 -35
  201. package/dist/delivery-manager-PFAKEJTC.js +0 -32
  202. package/dist/down-FWWTEKXM.js +0 -15
  203. package/dist/env-JCOF2222.js +0 -191
  204. package/dist/export-SUYRLI5Q.js +0 -112
  205. package/dist/extension-OBTGKQQD.js +0 -175
  206. package/dist/extensions-KYNTVTMO.js +0 -30
  207. package/dist/files-65PMW5IK.js +0 -47
  208. package/dist/history-DKCDI3JO.js +0 -128
  209. package/dist/import-DDUFE7AY.js +0 -23
  210. package/dist/join-I5QEE3LG.js +0 -66
  211. package/dist/list-JQ463EDA.js +0 -41
  212. package/dist/login-D7ETSU4R.js +0 -47
  213. package/dist/login-RIJF2F4G.js +0 -47
  214. package/dist/logout-5MLHZALK.js +0 -40
  215. package/dist/logout-UZJRGY4Z.js +0 -21
  216. package/dist/message-delivery-DFF5SJRM.js +0 -42
  217. package/dist/mind-IOJFLEM5.js +0 -108
  218. package/dist/mind-activity-tracker-F6O4Q2SL.js +0 -18
  219. package/dist/mind-list-WUPMQDYQ.js +0 -30
  220. package/dist/mind-manager-NBJF5D26.js +0 -32
  221. package/dist/mind-profile-P67FEHOY.js +0 -47
  222. package/dist/mind-service-2MQ6UK5N.js +0 -38
  223. package/dist/mind-sleep-WW2IX7JT.js +0 -42
  224. package/dist/mind-status-L3EFFRPR.js +0 -56
  225. package/dist/mind-wake-VSSGW465.js +0 -37
  226. package/dist/read-EBY56C33.js +0 -75
  227. package/dist/register-HD74C4TT.js +0 -47
  228. package/dist/reject-UJKFBHRO.js +0 -40
  229. package/dist/restart-3UCMRUVC.js +0 -33
  230. package/dist/scheduler-ZZ7XGQG6.js +0 -32
  231. package/dist/seed-check-S2IX25RL.js +0 -32
  232. package/dist/seed-cmd-DKOUFEAU.js +0 -36
  233. package/dist/seed-create-4XBBOLRH.js +0 -112
  234. package/dist/seed-sprout-GQEIIQRT.js +0 -132
  235. package/dist/send-QIV2INHB.js +0 -373
  236. package/dist/skill-PSQGRRJX.js +0 -358
  237. package/dist/skills/shared-files/SKILL.md +0 -44
  238. package/dist/skills/shared-files/scripts/merge.ts +0 -72
  239. package/dist/skills/shared-files/scripts/pull.ts +0 -52
  240. package/dist/sleep-manager-JTXSN7NV.js +0 -36
  241. package/dist/spirit-VRONKFMF.js +0 -23
  242. package/dist/split-STOROBYJ.js +0 -63
  243. package/dist/start-K2NCUUCG.js +0 -33
  244. package/dist/status-3JBTFSMI.js +0 -115
  245. package/dist/stop-H26JZDXF.js +0 -32
  246. package/dist/system-chat-JAPOJ3KE.js +0 -36
  247. package/dist/systems-XRI52VCH.js +0 -61
  248. package/dist/template-hash-A6VVKOXJ.js +0 -9
  249. package/dist/up-M5AS6SBV.js +0 -18
  250. package/dist/update-UD543CXX.js +0 -215
  251. package/dist/upgrade-O4Q7WJM3.js +0 -67
  252. package/dist/variant-7TGZHOU3.js +0 -41
  253. package/dist/web-assets/assets/index-CWJrVveV.css +0 -1
  254. package/dist/web-assets/assets/index-DJt14FRI.js +0 -75
  255. package/packages/extensions/pages/dist/ui/assets/index-tLTROSk5.js +0 -2
@@ -1,6 +1,11 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { formatPrefix, formatTypingSuffix } from "./format-prefix.js";
3
+ import {
4
+ compactTime,
5
+ compactTimestamp,
6
+ formatPrefix,
7
+ formatTypingSuffix,
8
+ } from "./format-prefix.js";
4
9
  import { log, warn } from "./logger.js";
5
10
  import {
6
11
  type BatchConfig,
@@ -11,6 +16,12 @@ import {
11
16
  import { loadPrompts } from "./startup.js";
12
17
  import type { ChannelMeta, HandlerResolver, Listener, VoluteContentPart } from "./types.js";
13
18
 
19
+ /** Shape of a single message in a batch payload (subset of daemon DeliveryPayload). */
20
+ export type BatchMessage = {
21
+ sender?: string | null;
22
+ content: unknown;
23
+ };
24
+
14
25
  export type Router = {
15
26
  route(
16
27
  content: VoluteContentPart[],
@@ -25,7 +36,7 @@ export type Router = {
25
36
  listener?: Listener,
26
37
  ): { messageId: string; unsubscribe: () => void };
27
38
  dispatchBatch(
28
- batch: { channels: Record<string, any[]> },
39
+ batch: { channels: Record<string, BatchMessage[]> },
29
40
  session: string,
30
41
  meta: ChannelMeta,
31
42
  ): void;
@@ -55,7 +66,7 @@ function generateMessageId(): string {
55
66
  }
56
67
 
57
68
  function applyPrefix(content: VoluteContentPart[], meta: ChannelMeta): VoluteContentPart[] {
58
- const time = new Date().toLocaleString();
69
+ const time = compactTimestamp();
59
70
  const prefix = formatPrefix(meta, time);
60
71
  if (!prefix) return content;
61
72
 
@@ -129,7 +140,7 @@ function formatInviteNotification(
129
140
  filePath: string,
130
141
  messageText: string,
131
142
  ): string {
132
- const time = new Date().toLocaleString();
143
+ const time = compactTimestamp();
133
144
  const prompts = loadPrompts();
134
145
 
135
146
  const headerLines: string[] = [];
@@ -172,6 +183,18 @@ export function createRouter(options: {
172
183
  }): Router {
173
184
  const batchBuffers = new Map<string, BatchBuffer>();
174
185
  const pendingChannels = new Set<string>();
186
+ const instructedSessions = new Set<string>();
187
+
188
+ /** Prepend session instructions only on the first message per session. */
189
+ function prependInstructionsOnce(
190
+ content: VoluteContentPart[],
191
+ instructions: string | undefined,
192
+ sessionName: string,
193
+ ): VoluteContentPart[] {
194
+ if (!instructions || instructedSessions.has(sessionName)) return content;
195
+ instructedSessions.add(sessionName);
196
+ return prependInstructions(content, instructions);
197
+ }
175
198
 
176
199
  function flushBatch(key: string) {
177
200
  const buffer = batchBuffers.get(key);
@@ -191,16 +214,22 @@ export function createRouter(options: {
191
214
  const uri = msg.channel ?? "unknown";
192
215
  channelCounts.set(uri, (channelCounts.get(uri) ?? 0) + 1);
193
216
  }
194
- const channelLabels = [...channelCounts.entries()].map(([uri, n]) => {
217
+ let header: string;
218
+ if (channelCounts.size === 1) {
219
+ const [uri] = [...channelCounts.keys()];
195
220
  const msg = messages.find((m) => m.channel === uri);
196
- const display = msg?.channelName
197
- ? `#${msg.channelName}${msg.serverName ? ` in ${msg.serverName}` : ""} (${uri})`
198
- : uri;
199
- return `${n} from ${display}`;
200
- });
201
- const summary = channelLabels.join(", ");
202
-
203
- const header = `[Batch: ${messages.length} message${messages.length === 1 ? "" : "s"} — ${summary}]`;
221
+ const display = msg?.channelName ? `#${msg.channelName}` : uri;
222
+ header = `[Batch: ${messages.length} message${messages.length === 1 ? "" : "s"} from ${display}]`;
223
+ } else {
224
+ const channelLabels = [...channelCounts.entries()].map(([uri, n]) => {
225
+ const msg = messages.find((m) => m.channel === uri);
226
+ const display = msg?.channelName
227
+ ? `#${msg.channelName}${msg.serverName ? ` in ${msg.serverName}` : ""}`
228
+ : uri;
229
+ return `${n} from ${display}`;
230
+ });
231
+ header = `[Batch: ${messages.length} messages — ${channelLabels.join(", ")}]`;
232
+ }
204
233
  // Include channel URI per message when batch spans multiple channels
205
234
  const multiChannel = channelCounts.size > 1;
206
235
  const body = messages
@@ -222,7 +251,7 @@ export function createRouter(options: {
222
251
  // Resolve session config for instructions
223
252
  const config = options.configPath ? loadRoutingConfig(options.configPath) : {};
224
253
  const sessionConfig = resolveSessionConfig(config, buffer.sessionName);
225
- content = prependInstructions(content, sessionConfig.instructions);
254
+ content = prependInstructionsOnce(content, sessionConfig.instructions, buffer.sessionName);
226
255
 
227
256
  const messageId = generateMessageId();
228
257
  const handler = options.mindHandler(buffer.sessionName);
@@ -296,13 +325,23 @@ export function createRouter(options: {
296
325
  // Resolve session config for instructions
297
326
  const config = options.configPath ? loadRoutingConfig(options.configPath) : {};
298
327
  const sessionConfig = resolveSessionConfig(config, session);
299
- const withInstructions = prependInstructions(withTyping, sessionConfig.instructions);
328
+ const withInstructions = prependInstructionsOnce(
329
+ withTyping,
330
+ sessionConfig.instructions,
331
+ session,
332
+ );
300
333
 
301
334
  const handler = options.mindHandler(session);
302
- const interrupt = (meta as any).interrupt ?? sessionConfig.interrupt;
335
+ const interrupt = meta.interrupt ?? sessionConfig.interrupt;
303
336
  const unsubscribe = handler.handle(
304
337
  withInstructions,
305
- { ...meta, sessionName: session, messageId, interrupt },
338
+ {
339
+ ...meta,
340
+ sessionName: session,
341
+ messageId,
342
+ interrupt,
343
+ replyInstructions: sessionConfig.replyInstructions,
344
+ },
306
345
  safeListener,
307
346
  );
308
347
  return { messageId, unsubscribe };
@@ -423,10 +462,7 @@ export function createRouter(options: {
423
462
  channel: meta.channel,
424
463
  channelName: meta.channelName,
425
464
  serverName: meta.serverName,
426
- timestamp: new Date().toLocaleTimeString("en-US", {
427
- hour: "numeric",
428
- minute: "2-digit",
429
- }),
465
+ timestamp: compactTime(),
430
466
  typing: meta.typing,
431
467
  });
432
468
 
@@ -444,7 +480,11 @@ export function createRouter(options: {
444
480
  // Direct dispatch to mind
445
481
  const formatted = applyPrefix(content, { ...meta, sessionName });
446
482
  const withTyping = appendTypingSuffix(formatted, meta.typing);
447
- const withInstructions = prependInstructions(withTyping, sessionConfig.instructions);
483
+ const withInstructions = prependInstructionsOnce(
484
+ withTyping,
485
+ sessionConfig.instructions,
486
+ sessionName,
487
+ );
448
488
  const handler = options.mindHandler(sessionName);
449
489
  const unsubscribe = handler.handle(
450
490
  withInstructions,
@@ -453,6 +493,7 @@ export function createRouter(options: {
453
493
  sessionName,
454
494
  messageId,
455
495
  interrupt: sessionConfig.interrupt,
496
+ replyInstructions: sessionConfig.replyInstructions,
456
497
  },
457
498
  safeListener,
458
499
  );
@@ -464,11 +505,11 @@ export function createRouter(options: {
464
505
  * Formats messages grouped by channel into a single SDK message.
465
506
  */
466
507
  function dispatchBatch(
467
- batch: { channels: Record<string, any[]> },
508
+ batch: { channels: Record<string, BatchMessage[]> },
468
509
  session: string,
469
510
  _meta: ChannelMeta,
470
511
  ): void {
471
- const allMessages: { channel: string; payload: any }[] = [];
512
+ const allMessages: { channel: string; payload: BatchMessage }[] = [];
472
513
  for (const [channel, messages] of Object.entries(batch.channels)) {
473
514
  for (const msg of messages) {
474
515
  allMessages.push({ channel, payload: msg });
@@ -482,10 +523,14 @@ export function createRouter(options: {
482
523
  for (const msg of allMessages) {
483
524
  channelCounts.set(msg.channel, (channelCounts.get(msg.channel) ?? 0) + 1);
484
525
  }
485
- const channelLabels = [...channelCounts.entries()].map(([ch, n]) => `${n} from ${ch}`);
486
- const summary = channelLabels.join(", ");
487
-
488
- const header = `[Batch: ${allMessages.length} message${allMessages.length === 1 ? "" : "s"} ${summary}]`;
526
+ let header: string;
527
+ if (channelCounts.size === 1) {
528
+ const [ch] = [...channelCounts.keys()];
529
+ header = `[Batch: ${allMessages.length} message${allMessages.length === 1 ? "" : "s"} from ${ch}]`;
530
+ } else {
531
+ const channelLabels = [...channelCounts.entries()].map(([ch, n]) => `${n} from ${ch}`);
532
+ header = `[Batch: ${allMessages.length} messages — ${channelLabels.join(", ")}]`;
533
+ }
489
534
  const multiChannel = channelCounts.size > 1;
490
535
 
491
536
  const body = allMessages
@@ -500,10 +545,7 @@ export function createRouter(options: {
500
545
  .map((p) => p.text)
501
546
  .join("\n")
502
547
  : JSON.stringify(m.payload.content);
503
- const time = new Date().toLocaleTimeString("en-US", {
504
- hour: "numeric",
505
- minute: "2-digit",
506
- });
548
+ const time = compactTime();
507
549
  const prefix = multiChannel
508
550
  ? `[${sender} in ${m.channel} — ${time}]`
509
551
  : `[${sender} — ${time}]`;
@@ -516,7 +558,7 @@ export function createRouter(options: {
516
558
  // Resolve session config for instructions
517
559
  const config = options.configPath ? loadRoutingConfig(options.configPath) : {};
518
560
  const sessionConfig = resolveSessionConfig(config, session);
519
- const withInstructions = prependInstructions(content, sessionConfig.instructions);
561
+ const withInstructions = prependInstructionsOnce(content, sessionConfig.instructions, session);
520
562
 
521
563
  const messageId = generateMessageId();
522
564
  const handler = options.mindHandler(session);
@@ -23,12 +23,14 @@ export type SessionConfig = {
23
23
  batch?: number | BatchConfig;
24
24
  interrupt?: boolean;
25
25
  instructions?: string;
26
+ replyInstructions?: "once" | "always" | "never";
26
27
  };
27
28
 
28
29
  export type ResolvedSessionConfig = {
29
30
  batch?: BatchConfig;
30
31
  interrupt: boolean;
31
32
  instructions?: string;
33
+ replyInstructions: "once" | "always" | "never";
32
34
  };
33
35
 
34
36
  export type RoutingConfig = {
@@ -154,7 +156,7 @@ export function resolveSessionConfig(
154
156
  config: RoutingConfig,
155
157
  sessionName: string,
156
158
  ): ResolvedSessionConfig {
157
- const defaults: ResolvedSessionConfig = { interrupt: true };
159
+ const defaults: ResolvedSessionConfig = { interrupt: true, replyInstructions: "once" };
158
160
 
159
161
  if (!config.sessions) return defaults;
160
162
 
@@ -165,6 +167,7 @@ export function resolveSessionConfig(
165
167
  batch,
166
168
  interrupt: sessionConfig.interrupt ?? true,
167
169
  instructions: sessionConfig.instructions,
170
+ replyInstructions: sessionConfig.replyInstructions ?? "once",
168
171
  };
169
172
  }
170
173
  }
@@ -23,13 +23,19 @@ export type SubagentConfig = {
23
23
  maxTurns?: number;
24
24
  };
25
25
 
26
- export function loadConfig(): {
26
+ export type MindConfig = {
27
27
  model?: string;
28
28
  logLevel?: "error" | "warn" | "info" | "debug";
29
29
  compactionMessage?: string;
30
30
  compaction?: { maxContextTokens?: number };
31
31
  subagents?: Record<string, SubagentConfig>;
32
- } {
32
+ // Template-specific config fields (claude, pi, codex)
33
+ maxThinkingTokens?: number;
34
+ thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
35
+ reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
36
+ };
37
+
38
+ export function loadConfig(): MindConfig {
33
39
  try {
34
40
  return JSON.parse(readFileSync(resolve("home/.config/config.json"), "utf-8"));
35
41
  } catch (err: any) {
@@ -77,12 +83,16 @@ export function loadPackageInfo(): { name: string; version: string } {
77
83
  }
78
84
  }
79
85
 
80
- export async function handleStartupContext(sendMessage: (content: string) => void): Promise<void> {
86
+ /**
87
+ * Run the startup-context hook and return the generated context string.
88
+ * Returns null if no hook is found or it produces no output.
89
+ */
90
+ export async function getStartupContext(): Promise<string | null> {
81
91
  // Prefer .ts, fall back to .sh for backwards compatibility
82
92
  const tsPath = resolve("home/.local/hooks/startup-context.ts");
83
93
  const shPath = resolve("home/.local/hooks/startup-context.sh");
84
94
  const scriptPath = existsSync(tsPath) ? tsPath : existsSync(shPath) ? shPath : null;
85
- if (!scriptPath) return;
95
+ if (!scriptPath) return null;
86
96
 
87
97
  const isTs = scriptPath.endsWith(".ts");
88
98
 
@@ -112,12 +122,10 @@ export async function handleStartupContext(sendMessage: (content: string) => voi
112
122
  context = stdout.trim();
113
123
  }
114
124
 
115
- if (context) {
116
- sendMessage(`[system] ${context}`);
117
- log("server", "sent startup context");
118
- }
125
+ return context || null;
119
126
  } catch (e) {
120
127
  log("server", "failed to run startup context hook:", e);
128
+ return null;
121
129
  }
122
130
  }
123
131
 
@@ -21,6 +21,8 @@ export type ChannelMeta = {
21
21
  participantCount?: number;
22
22
  participantProfiles?: ParticipantProfile[];
23
23
  typing?: string[];
24
+ replyInstructions?: "once" | "always" | "never";
25
+ interrupt?: boolean;
24
26
  signature?: string;
25
27
  signatureTimestamp?: string;
26
28
  signerFingerprint?: string;
@@ -30,7 +32,6 @@ export type ChannelMeta = {
30
32
  /** ChannelMeta enriched by the router with dispatch info. */
31
33
  export type HandlerMeta = ChannelMeta & {
32
34
  messageId: string;
33
- interrupt?: boolean;
34
35
  };
35
36
 
36
37
  export type VoluteRequest = {
@@ -1,7 +1,7 @@
1
1
  import { createHash, verify } from "node:crypto";
2
2
  import { createServer, type IncomingMessage, type Server } from "node:http";
3
3
  import { log } from "./logger.js";
4
- import type { Router } from "./router.js";
4
+ import type { BatchMessage, Router } from "./router.js";
5
5
  import type { VoluteContentPart, VoluteRequest } from "./types.js";
6
6
 
7
7
  function readBody(req: IncomingMessage): Promise<string> {
@@ -21,10 +21,27 @@ function extractText(content: VoluteContentPart[] | string): string {
21
21
  .join("\n");
22
22
  }
23
23
 
24
- /** Normalize content to VoluteContentPart[] — connectors may send plain strings. */
24
+ /** Normalize content to VoluteContentPart[] — connectors may send plain strings or mixed arrays. */
25
25
  function normalizeContent(content: unknown): VoluteContentPart[] {
26
- if (Array.isArray(content)) return content as VoluteContentPart[];
27
26
  if (typeof content === "string") return [{ type: "text", text: content }];
27
+ if (Array.isArray(content)) {
28
+ return content.map((item): VoluteContentPart => {
29
+ if (typeof item === "object" && item !== null && "type" in item) {
30
+ const obj = item as Record<string, unknown>;
31
+ if (obj.type === "image") {
32
+ if (typeof obj.media_type === "string" && typeof obj.data === "string") {
33
+ return { type: "image", media_type: obj.media_type, data: obj.data };
34
+ }
35
+ log("server", "image content part missing required fields, coercing to text");
36
+ }
37
+ if (typeof obj.text === "string") return { type: "text", text: obj.text };
38
+ }
39
+ if (typeof item !== "string") {
40
+ log("server", `unexpected content type (${typeof item}), coercing to text`);
41
+ }
42
+ return { type: "text", text: typeof item === "string" ? item : JSON.stringify(item) };
43
+ });
44
+ }
28
45
  return [{ type: "text", text: JSON.stringify(content) }];
29
46
  }
30
47
 
@@ -78,15 +95,47 @@ async function verifyRequest(body: VoluteRequest): Promise<boolean | undefined>
78
95
  return verifySignature(publicKey, text, body.signatureTimestamp, body.signature);
79
96
  }
80
97
 
98
+ export type ContextBreakdown = {
99
+ systemPrompt: number;
100
+ sdkInstructions: number;
101
+ skillDescriptions: number;
102
+ conversation: {
103
+ userText: number;
104
+ assistantText: number;
105
+ thinking: number;
106
+ toolUse: number;
107
+ toolResult: number;
108
+ };
109
+ };
110
+
111
+ export type SessionContextInfo = {
112
+ name: string;
113
+ contextTokens: number;
114
+ contextWindow?: number;
115
+ breakdown?: ContextBreakdown;
116
+ };
117
+
118
+ export type ContextInfo = {
119
+ sessions: SessionContextInfo[];
120
+ systemPrompt: number;
121
+ };
122
+
81
123
  export function createVoluteServer(options: {
82
124
  router: Router;
83
125
  port: number;
84
126
  name: string;
85
127
  version: string;
128
+ getContextInfo?: () => ContextInfo | Promise<ContextInfo>;
86
129
  }): Server {
87
130
  const { router, port, name, version } = options;
88
131
 
89
132
  const server = createServer(async (req, res) => {
133
+ // Prevent EPIPE crashes when the client disconnects before the response is written
134
+ res.on("error", (err: NodeJS.ErrnoException) => {
135
+ if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
136
+ log("server", "response error:", err);
137
+ });
138
+
90
139
  const url = new URL(req.url!, "http://localhost");
91
140
 
92
141
  if (req.method === "GET" && url.pathname === "/health") {
@@ -95,6 +144,24 @@ export function createVoluteServer(options: {
95
144
  return;
96
145
  }
97
146
 
147
+ if (req.method === "GET" && url.pathname === "/context") {
148
+ if (!options.getContextInfo) {
149
+ res.writeHead(404);
150
+ res.end("Not Found");
151
+ return;
152
+ }
153
+ try {
154
+ const info = await options.getContextInfo();
155
+ res.writeHead(200, { "Content-Type": "application/json" });
156
+ res.end(JSON.stringify(info));
157
+ } catch (err) {
158
+ log("server", "error in /context:", err);
159
+ res.writeHead(500);
160
+ res.end("Internal Server Error");
161
+ }
162
+ return;
163
+ }
164
+
98
165
  if (req.method === "POST" && url.pathname === "/message") {
99
166
  try {
100
167
  const body = JSON.parse(await readBody(req)) as VoluteRequest;
@@ -110,11 +177,11 @@ export function createVoluteServer(options: {
110
177
  body.content = normalizeContent(body.content);
111
178
 
112
179
  // Handle batch payloads from delivery manager
113
- if ((body as any).batch) {
114
- const batch = (body as any).batch as {
115
- channels: Record<string, any[]>;
116
- };
117
- router.dispatchBatch(batch, body.session ?? "main", body);
180
+ const bodyWithBatch = body as VoluteRequest & {
181
+ batch?: { channels: Record<string, BatchMessage[]> };
182
+ };
183
+ if (bodyWithBatch.batch) {
184
+ router.dispatchBatch(bodyWithBatch.batch, body.session ?? "main", body);
118
185
  } else {
119
186
  // Pre-routed by daemon delivery manager — dispatch directly
120
187
  router.dispatch(body.content, body.session ?? "main", body);
@@ -6,21 +6,22 @@ You are an autonomous mind running as a persistent server in a git repository. Y
6
6
 
7
7
  Messages arrive with a context prefix:
8
8
  ```
9
- [Discord: username in #general in My Server — 1/15/2025, 10:30:00 AM]
9
+ [Discord: username in #general in My Server — 2025-01-15 10:30]
10
10
  ```
11
11
 
12
12
  You can also reach out proactively — see the **volute-mind** skill.
13
13
 
14
- ## Identity Files
14
+ ## Identity & Sessions
15
15
 
16
16
  These files shape your starting identity. They're loaded into your system prompt, but they belong to you — edit them as you evolve:
17
17
 
18
18
  - `SOUL.md` — Who you are. Your core sense of self.
19
19
  - `MEMORY.md` — What you know. Your long-term memory.
20
- - `VOLUTE.md` — How you communicate. Your channels and routing.
21
20
 
22
21
  **Editing any identity file triggers an automatic restart** — your server restarts so the updated file takes effect. Your session resumes automatically.
23
22
 
23
+ You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/routes.json`. Your conversation may be **resumed** from a previous session — orient yourself by reading recent journal entries if needed. On a **fresh session**, read `MEMORY.md` and recent journal entries to remember where you left off. On **compaction**, update today's journal to preserve context before the conversation is trimmed.
24
+
24
25
  ## Memory System
25
26
 
26
27
  Two-tier memory, both managed via file tools:
@@ -30,10 +31,3 @@ Two-tier memory, both managed via file tools:
30
31
  - Periodically consolidate journal entries into `MEMORY.md` to promote lasting insights.
31
32
 
32
33
  See the **memory** skill for detailed guidance.
33
-
34
- ## Sessions
35
-
36
- - You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/routes.json`.
37
- - Your conversation may be **resumed** from a previous session — orient yourself by reading recent journal entries if needed.
38
- - On a **fresh session**, read `MEMORY.md` and recent journal entries to remember where you left off.
39
- - On **compaction**, update today's journal to preserve context before the conversation is trimmed.
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@anthropic-ai/claude-agent-sdk": "^0.2.34",
13
+ "@anthropic-ai/tokenizer": "^0.0.4",
13
14
  "tsx": "^4.0.0"
14
15
  },
15
16
  "devDependencies": {