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,8 +1,15 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { resolve as resolvePath } from "node:path";
3
- import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
3
+ import type { HookCallback, SyncHookJSONOutput } from "@anthropic-ai/claude-agent-sdk";
4
4
  import { query } from "@anthropic-ai/claude-agent-sdk";
5
5
  import { toSDKContent } from "./lib/content.js";
6
+ import {
7
+ countSdkInstructionTokens,
8
+ countSkillDescriptionTokens,
9
+ countSystemPromptTokens,
10
+ findClaudeSessionFile,
11
+ parseClaudeSessionJSONL,
12
+ } from "./lib/context-breakdown.js";
6
13
  import { daemonEmit } from "./lib/daemon-client.js";
7
14
  import { runHooks } from "./lib/hook-loader.js";
8
15
  import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
@@ -23,6 +30,7 @@ import type {
23
30
  VoluteContentPart,
24
31
  VoluteEvent,
25
32
  } from "./lib/types.js";
33
+ import type { ContextInfo } from "./lib/volute-server.js";
26
34
 
27
35
  type Session = {
28
36
  name: string;
@@ -31,7 +39,10 @@ type Session = {
31
39
  messageIds: (string | undefined)[];
32
40
  currentMessageId?: string;
33
41
  currentQuery?: ReturnType<typeof query>;
34
- messageChannels: Map<string, string>;
42
+ messageChannels: Map<string, { channel: string; sender?: string }>;
43
+ replyInstructionsFired: boolean;
44
+ replyInstructionsMode: "once" | "always" | "never";
45
+ contextTokens: number;
35
46
  };
36
47
 
37
48
  export function createMind(options: {
@@ -45,7 +56,11 @@ export function createMind(options: {
45
56
  maxContextTokens?: number;
46
57
  subagents?: Record<string, SubagentConfig>;
47
58
  onIdentityReload?: () => Promise<void>;
48
- }): { resolve: HandlerResolver; waitForCommits: () => Promise<void> } {
59
+ }): {
60
+ resolve: HandlerResolver;
61
+ waitForCommits: () => Promise<void>;
62
+ getContextInfo: () => ContextInfo;
63
+ } {
49
64
  const autoCommit = createAutoCommitHook(options.cwd);
50
65
  const identityReload = createIdentityReloadHook(options.cwd);
51
66
  const sessionStore = createSessionStore(options.sessionsDir);
@@ -135,11 +150,16 @@ export function createMind(options: {
135
150
  function wrapHookWithEmit(hook: HookCallback, source: string, session: Session): HookCallback {
136
151
  return async (...args) => {
137
152
  const result = await hook(...args);
138
- const additionalContext = (result as any)?.hookSpecificOutput?.additionalContext;
139
- const decision = (result as any)?.decision;
153
+ const syncResult = result as SyncHookJSONOutput;
154
+ const hookOutput = syncResult?.hookSpecificOutput;
155
+ const additionalContext =
156
+ hookOutput && "additionalContext" in hookOutput
157
+ ? (hookOutput.additionalContext as string | undefined)
158
+ : undefined;
159
+ const decision = syncResult?.decision;
140
160
  if (additionalContext || decision) {
141
161
  const channel = session.currentMessageId
142
- ? session.messageChannels.get(session.currentMessageId)
162
+ ? session.messageChannels.get(session.currentMessageId)?.channel
143
163
  : undefined;
144
164
  try {
145
165
  daemonEmit({
@@ -164,7 +184,7 @@ export function createMind(options: {
164
184
  const result = await runHooks(hooksDir, event, input as Record<string, unknown>);
165
185
  if (result.additionalContext || Object.keys(result.metadata).length > 0) {
166
186
  const channel = session.currentMessageId
167
- ? session.messageChannels.get(session.currentMessageId)
187
+ ? session.messageChannels.get(session.currentMessageId)?.channel
168
188
  : undefined;
169
189
  try {
170
190
  daemonEmit({
@@ -202,7 +222,7 @@ export function createMind(options: {
202
222
  preCompactHook: HookCallback,
203
223
  resume?: string,
204
224
  ) {
205
- const replyInstructions = createReplyInstructionsHook(session.messageChannels);
225
+ const replyInstructions = createReplyInstructionsHook(session.messageChannels, session);
206
226
 
207
227
  return query({
208
228
  prompt: session.channel.iterable,
@@ -279,27 +299,30 @@ export function createMind(options: {
279
299
  options.onIdentityReload?.();
280
300
  }
281
301
  },
282
- onContextTokens: maxContextTokens
283
- ? (tokens: number) => {
284
- if (tokens >= maxContextTokens && !compactionTriggered.get(session.name)) {
285
- compactionTriggered.set(session.name, true);
286
- log(
287
- "mind",
288
- `session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — triggering compaction`,
289
- );
290
- session.messageIds.push(undefined);
291
- session.channel.push({
292
- type: "user",
293
- session_id: "",
294
- message: {
295
- role: "user",
296
- content: [{ type: "text", text: compactionMessage }],
297
- },
298
- parent_tool_use_id: null,
299
- });
300
- }
301
- }
302
- : undefined,
302
+ onContextTokens: (tokens: number) => {
303
+ session.contextTokens = tokens;
304
+ if (
305
+ maxContextTokens &&
306
+ tokens >= maxContextTokens &&
307
+ !compactionTriggered.get(session.name)
308
+ ) {
309
+ compactionTriggered.set(session.name, true);
310
+ log(
311
+ "mind",
312
+ `session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — triggering compaction`,
313
+ );
314
+ session.messageIds.push(undefined);
315
+ session.channel.push({
316
+ type: "user",
317
+ session_id: "",
318
+ message: {
319
+ role: "user",
320
+ content: [{ type: "text", text: compactionMessage }],
321
+ },
322
+ parent_tool_use_id: null,
323
+ });
324
+ }
325
+ },
303
326
  };
304
327
 
305
328
  async function runCompact(sessionId: string) {
@@ -427,11 +450,21 @@ export function createMind(options: {
427
450
  listeners: new Set(),
428
451
  messageIds: [],
429
452
  messageChannels: new Map(),
453
+ replyInstructionsFired: false,
454
+ replyInstructionsMode: "once",
455
+ contextTokens: 0,
430
456
  };
431
457
  sessions.set(name, session);
432
458
 
433
459
  const isEphemeral = name.startsWith("new-");
434
- const savedSessionId = isEphemeral ? undefined : sessionStore.load(name);
460
+ let savedSessionId = isEphemeral ? undefined : sessionStore.load(name);
461
+ // Validate that the SDK session file still exists — orphaned references
462
+ // cause the SDK to throw and can crash the process with EPIPE.
463
+ if (savedSessionId && !findClaudeSessionFile(options.cwd, savedSessionId)) {
464
+ log("mind", `session "${name}": stored session ${savedSessionId} not found, starting fresh`);
465
+ sessionStore.delete(name);
466
+ savedSessionId = undefined;
467
+ }
435
468
  if (savedSessionId) {
436
469
  log("mind", `session "${name}": resuming ${savedSessionId}`);
437
470
  } else {
@@ -455,9 +488,17 @@ export function createMind(options: {
455
488
  };
456
489
  session.listeners.add(filteredListener);
457
490
 
458
- // Track channel for reply instructions
491
+ // Track channel/sender for reply instructions
459
492
  if (meta.channel) {
460
- session.messageChannels.set(meta.messageId, meta.channel);
493
+ session.messageChannels.set(meta.messageId, {
494
+ channel: meta.channel,
495
+ sender: meta.sender,
496
+ });
497
+ }
498
+
499
+ // Update reply instructions mode from routing config
500
+ if (meta.replyInstructions) {
501
+ session.replyInstructionsMode = meta.replyInstructions;
461
502
  }
462
503
 
463
504
  // Interrupt if requested and session is mid-turn
@@ -497,5 +538,39 @@ export function createMind(options: {
497
538
  return handler;
498
539
  }
499
540
 
500
- return { resolve, waitForCommits: autoCommit.waitForCommits };
541
+ const systemPromptTokens = countSystemPromptTokens(options.systemPrompt);
542
+ const claudeMdTokens = countSdkInstructionTokens(options.cwd);
543
+ const skillDescTokens = countSkillDescriptionTokens([resolvePath(options.cwd, ".claude/skills")]);
544
+
545
+ function getContextInfo(): ContextInfo {
546
+ return {
547
+ sessions: Array.from(sessions.values()).map((s) => {
548
+ try {
549
+ const sessionId = sessionStore.load(s.name);
550
+ const jsonlPath = sessionId ? findClaudeSessionFile(options.cwd, sessionId) : null;
551
+ const parsed = jsonlPath
552
+ ? parseClaudeSessionJSONL(
553
+ jsonlPath,
554
+ systemPromptTokens,
555
+ claudeMdTokens,
556
+ skillDescTokens,
557
+ )
558
+ : null;
559
+
560
+ return {
561
+ name: s.name,
562
+ contextTokens: parsed?.contextTokens ?? s.contextTokens,
563
+ contextWindow: maxContextTokens,
564
+ breakdown: parsed?.breakdown,
565
+ };
566
+ } catch (err) {
567
+ log("mind", `failed to get context breakdown for session "${s.name}":`, err);
568
+ return { name: s.name, contextTokens: s.contextTokens, contextWindow: maxContextTokens };
569
+ }
570
+ }),
571
+ systemPrompt: systemPromptTokens,
572
+ };
573
+ }
574
+
575
+ return { resolve, waitForCommits: autoCommit.waitForCommits, getContextInfo };
501
576
  }
@@ -1,22 +1,42 @@
1
1
  import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { loadPrompts } from "../startup.js";
3
3
 
4
- export function createReplyInstructionsHook(messageChannels: Map<string, string>) {
5
- let fired = false;
4
+ export function createReplyInstructionsHook(
5
+ messageChannels: Map<string, { channel: string; sender?: string }>,
6
+ sessionState: {
7
+ replyInstructionsFired: boolean;
8
+ replyInstructionsMode: "once" | "always" | "never";
9
+ },
10
+ ) {
6
11
  const prompts = loadPrompts();
7
12
 
8
13
  const hook: HookCallback = async () => {
9
- if (fired) return {};
14
+ // "never" suppresses reply instructions entirely
15
+ if (sessionState.replyInstructionsMode === "never") return {};
10
16
 
11
- const channel = messageChannels.values().next().value;
12
- if (!channel) return {};
17
+ // "once" only fires on first message per session
18
+ if (sessionState.replyInstructionsMode === "once" && sessionState.replyInstructionsFired)
19
+ return {};
13
20
 
14
- fired = true;
21
+ const entry = messageChannels.values().next().value;
22
+ if (!entry) return {};
23
+
24
+ sessionState.replyInstructionsFired = true;
25
+
26
+ // System messages don't need reply instructions
27
+ if (entry.sender === "volute") {
28
+ return {
29
+ hookSpecificOutput: {
30
+ hookEventName: "UserPromptSubmit" as const,
31
+ additionalContext: "This is a system message — no reply is needed.",
32
+ },
33
+ };
34
+ }
15
35
 
16
36
  return {
17
37
  hookSpecificOutput: {
18
38
  hookEventName: "UserPromptSubmit" as const,
19
- additionalContext: prompts.reply_instructions.replace(/\$\{channel\}/g, channel),
39
+ additionalContext: prompts.reply_instructions.replace(/\$\{channel\}/g, entry.channel),
20
40
  },
21
41
  };
22
42
  };
@@ -8,7 +8,7 @@ export type StreamSession = {
8
8
  name: string;
9
9
  messageIds: (string | undefined)[];
10
10
  currentMessageId?: string;
11
- messageChannels: Map<string, string>;
11
+ messageChannels: Map<string, { channel: string; sender?: string }>;
12
12
  };
13
13
 
14
14
  export type StreamCallbacks = {
@@ -26,7 +26,7 @@ function emit(
26
26
  event: { type: EventType; content?: string; metadata?: Record<string, unknown> },
27
27
  ) {
28
28
  const channel = session.currentMessageId
29
- ? session.messageChannels.get(session.currentMessageId)
29
+ ? session.messageChannels.get(session.currentMessageId)?.channel
30
30
  : undefined;
31
31
  const filtered = filterEvent(preset, {
32
32
  ...event,
@@ -52,6 +52,7 @@ const server = createVoluteServer({
52
52
  port,
53
53
  name: pkg.name,
54
54
  version: pkg.version,
55
+ getContextInfo: mind.getContextInfo,
55
56
  });
56
57
 
57
58
  server.listen(port, () => {
@@ -9,6 +9,7 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
+ "@anthropic-ai/tokenizer": "^0.0.4",
12
13
  "@openai/codex-sdk": "^0.115.0",
13
14
  "tsx": "^4.0.0"
14
15
  },
@@ -3,11 +3,18 @@ import { resolve as resolvePath } from "node:path";
3
3
  import { Codex } from "@openai/codex-sdk";
4
4
  import { flushFileChanges, trackFileChange } from "./lib/auto-commit.js";
5
5
  import { extractText } from "./lib/content.js";
6
+ import {
7
+ countSdkInstructionTokens,
8
+ countSkillDescriptionTokens,
9
+ countSystemPromptTokens,
10
+ findCodexSessionFile,
11
+ parseCodexSessionJSONL,
12
+ } from "./lib/context-breakdown.js";
6
13
  import { daemonEmit, daemonRestart, type EventType } from "./lib/daemon-client.js";
7
14
  import { runHooks } from "./lib/hook-loader.js";
8
15
  import { log, warn } from "./lib/logger.js";
9
16
  import { createSessionStore } from "./lib/session-store.js";
10
- import { loadPrompts, loadSystemPrompt } from "./lib/startup.js";
17
+ import { getStartupContext, loadPrompts, loadSystemPrompt } from "./lib/startup.js";
11
18
  import { filterEvent, loadTransparencyPreset } from "./lib/transparency.js";
12
19
  import type {
13
20
  HandlerMeta,
@@ -17,6 +24,7 @@ import type {
17
24
  VoluteContentPart,
18
25
  VoluteEvent,
19
26
  } from "./lib/types.js";
27
+ import type { ContextInfo } from "./lib/volute-server.js";
20
28
 
21
29
  /** Minimal interface for a Codex SDK thread — typed to the methods we actually use */
22
30
  type CodexThread = {
@@ -63,9 +71,9 @@ export function createMind(options: {
63
71
  cwd: string;
64
72
  mindDir: string;
65
73
  model?: string;
66
- reasoningEffort?: string;
74
+ reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
67
75
  maxContextTokens?: number;
68
- }): { resolve: HandlerResolver } {
76
+ }): { resolve: HandlerResolver; getContextInfo: () => ContextInfo } {
69
77
  const sessions = new Map<string, CodexSession>();
70
78
  const prompts = loadPrompts();
71
79
  const maxContextTokens = options.maxContextTokens;
@@ -76,6 +84,7 @@ export function createMind(options: {
76
84
 
77
85
  const sessionStore = createSessionStore(resolvePath(options.mindDir, ".mind/codex-sessions"));
78
86
  const hooksDir = resolvePath(options.cwd, ".local/hooks");
87
+ const startupContextPromise = getStartupContext().catch(() => null);
79
88
 
80
89
  // Write system prompt to file for Codex model_instructions_file
81
90
  const promptPath = resolvePath(options.mindDir, ".mind/system-prompt.md");
@@ -99,6 +108,9 @@ export function createMind(options: {
99
108
  model_instructions_file: promptPath,
100
109
  // Let the SDK handle compaction natively when a threshold is configured
101
110
  model_auto_compact_token_limit: maxContextTokens ?? 999999999,
111
+ // Enable reasoning summaries so they appear as events
112
+ model_reasoning_summary: "auto",
113
+ model_supports_reasoning_summaries: true,
102
114
  // The codex sandbox runs commands in /bin/zsh -lc which resets the environment.
103
115
  // Set ZDOTDIR so the login shell sources our .zshenv with VOLUTE env vars and PATH.
104
116
  shell_environment_policy: {
@@ -109,6 +121,9 @@ export function createMind(options: {
109
121
  },
110
122
  });
111
123
 
124
+ // Track which sessions have received startup context
125
+ const startupContextInjected = new Set<string>();
126
+
112
127
  // --- Session lifecycle ---
113
128
 
114
129
  function getOrCreateSession(name: string): CodexSession {
@@ -144,6 +159,7 @@ export function createMind(options: {
144
159
  session.thread = codex.resumeThread(savedThreadId, {
145
160
  workingDirectory: options.cwd,
146
161
  model: options.model,
162
+ modelReasoningEffort: options.reasoningEffort,
147
163
  skipGitRepoCheck: true,
148
164
  sandboxMode: "danger-full-access",
149
165
  });
@@ -158,6 +174,7 @@ export function createMind(options: {
158
174
  session.thread = codex.startThread({
159
175
  workingDirectory: options.cwd,
160
176
  model: options.model,
177
+ modelReasoningEffort: options.reasoningEffort,
161
178
  skipGitRepoCheck: true,
162
179
  sandboxMode: "danger-full-access",
163
180
  });
@@ -193,6 +210,20 @@ export function createMind(options: {
193
210
  // Refresh system prompt before each turn (picks up MEMORY.md changes)
194
211
  refreshSystemPrompt();
195
212
 
213
+ // Inject startup context on the first turn of each session
214
+ if (!startupContextInjected.has(session.name)) {
215
+ startupContextInjected.add(session.name);
216
+ const startupContext = await startupContextPromise;
217
+ if (startupContext) {
218
+ emit(session, {
219
+ type: "context",
220
+ content: startupContext,
221
+ metadata: { source: "startup-context" },
222
+ });
223
+ text = `${startupContext}\n\n${text}`;
224
+ }
225
+ }
226
+
196
227
  // Run pre-prompt hooks
197
228
  try {
198
229
  const hookResult = await runHooks(hooksDir, "pre-prompt", {
@@ -211,11 +242,14 @@ export function createMind(options: {
211
242
  warn("mind", "pre-prompt hook failed:", err);
212
243
  }
213
244
 
214
- // Reply instructions on first message per channel
245
+ // Reply instructions on first message per channel (skip system messages)
215
246
  const channel = meta.channel;
216
247
  if (channel && !session.firstMessagePerChannel.has(channel)) {
217
248
  session.firstMessagePerChannel.add(channel);
218
- const replyInstructions = prompts.reply_instructions.replace(/\$\{channel\}/g, channel);
249
+ const isSystem = meta.sender === "volute";
250
+ const replyInstructions = isSystem
251
+ ? "This is a system message — no reply is needed."
252
+ : prompts.reply_instructions.replace(/\$\{channel\}/g, channel);
219
253
  emit(session, {
220
254
  type: "context",
221
255
  content: replyInstructions,
@@ -281,7 +315,9 @@ export function createMind(options: {
281
315
  if (item.type === "agent_message" || item.type === "agentMessage") {
282
316
  itemText.set(event.itemId ?? item.id, "");
283
317
  } else if (item.type === "reasoning") {
284
- emit(session, { type: "thinking", content: item.content ?? "" });
318
+ // Reasoning text may arrive on started or completed
319
+ const text = item.text ?? item.content ?? "";
320
+ if (text) emit(session, { type: "thinking", content: text });
285
321
  } else if (item.type === "command_execution" || item.type === "commandExecution") {
286
322
  const cmd = item.command ?? item.args?.join(" ") ?? "";
287
323
  emit(session, {
@@ -356,7 +392,10 @@ export function createMind(options: {
356
392
  if (!item) break;
357
393
  const itemType = item.type;
358
394
 
359
- if (itemType === "agent_message" || itemType === "agentMessage") {
395
+ if (itemType === "reasoning") {
396
+ const text = item.text ?? item.content ?? "";
397
+ if (text) emit(session, { type: "thinking", content: text });
398
+ } else if (itemType === "agent_message" || itemType === "agentMessage") {
360
399
  // Emit any remaining delta
361
400
  const id = event.itemId ?? item.id;
362
401
  const prev = itemText.get(id) ?? "";
@@ -550,5 +589,38 @@ export function createMind(options: {
550
589
  return handler;
551
590
  }
552
591
 
553
- return { resolve };
592
+ const systemPromptTokens = countSystemPromptTokens(options.systemPrompt);
593
+ const claudeMdTokens = countSdkInstructionTokens(options.cwd);
594
+ const skillDescTokens = countSkillDescriptionTokens([resolvePath(options.cwd, ".agents/skills")]);
595
+
596
+ function getContextInfo(): ContextInfo {
597
+ return {
598
+ sessions: Array.from(sessions.values()).map((s) => {
599
+ try {
600
+ const threadId = sessionStore.load(s.name);
601
+ const jsonlPath = threadId ? findCodexSessionFile(threadId) : null;
602
+ const parsed = jsonlPath
603
+ ? parseCodexSessionJSONL(jsonlPath, systemPromptTokens, claudeMdTokens, skillDescTokens)
604
+ : null;
605
+
606
+ return {
607
+ name: s.name,
608
+ contextTokens: parsed?.contextTokens ?? s.cumulativeInputTokens,
609
+ contextWindow: maxContextTokens,
610
+ breakdown: parsed?.breakdown,
611
+ };
612
+ } catch (err) {
613
+ log("mind", `failed to get context breakdown for session "${s.name}":`, err);
614
+ return {
615
+ name: s.name,
616
+ contextTokens: s.cumulativeInputTokens,
617
+ contextWindow: maxContextTokens,
618
+ };
619
+ }
620
+ }),
621
+ systemPrompt: systemPromptTokens,
622
+ };
623
+ }
624
+
625
+ return { resolve, getContextInfo };
554
626
  }
@@ -4,7 +4,6 @@ import { createFileHandlerResolver } from "./lib/file-handler.js";
4
4
  import { log, setLevel } from "./lib/logger.js";
5
5
  import { createRouter } from "./lib/router.js";
6
6
  import {
7
- handleStartupContext,
8
7
  loadConfig,
9
8
  loadPackageInfo,
10
9
  loadSystemPrompt,
@@ -45,15 +44,13 @@ const server = createVoluteServer({
45
44
  port,
46
45
  name: pkg.name,
47
46
  version: pkg.version,
47
+ getContextInfo: mind.getContextInfo,
48
48
  });
49
49
 
50
50
  server.listen(port, async () => {
51
51
  const addr = server.address();
52
52
  const actualPort = typeof addr === "object" && addr ? addr.port : port;
53
53
  log("server", `listening on :${actualPort}`);
54
- await handleStartupContext((content) =>
55
- router.route([{ type: "text", text: content }], { channel: "system" }),
56
- );
57
54
  });
58
55
 
59
56
  setupShutdown();
@@ -9,6 +9,7 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
+ "@anthropic-ai/tokenizer": "^0.0.4",
12
13
  "@mariozechner/pi-coding-agent": "^0.58.1",
13
14
  "@sinclair/typebox": "^0.34.0",
14
15
  "tsx": "^4.0.0"