volute 0.33.0 → 0.34.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 (182) hide show
  1. package/dist/{accept-D5VBM7JW.js → accept-TW6V4WI4.js} +6 -6
  2. package/dist/{activity-events-XJO3P4RR.js → activity-events-BN7V6KCC.js} +4 -4
  3. package/dist/{ai-service-SBY2WG7O.js → ai-service-PSILB5WD.js} +5 -5
  4. package/dist/{api-client-YPKOZP2O.js → api-client-XUXOB7LI.js} +1 -1
  5. package/dist/api.d.ts +426 -3
  6. package/dist/{archive-INXYFVCW.js → archive-C2VEMQOR.js} +4 -4
  7. package/dist/{auth-GKCDSO4T.js → auth-ZFZXJZDQ.js} +5 -5
  8. package/dist/{bridge-TXWWPPOJ.js → bridge-O753D5F4.js} +6 -6
  9. package/dist/{chat-U5ZOME3O.js → chat-BHYX7DJ4.js} +9 -9
  10. package/dist/{chunk-M7UL5S3Q.js → chunk-2IOP6PHB.js} +1 -1
  11. package/dist/{chunk-NPKSDYA2.js → chunk-47XDEWWV.js} +5 -5
  12. package/dist/{chunk-RSX4OPZY.js → chunk-47ZPNLF4.js} +7 -7
  13. package/dist/{chunk-RPZZSXV3.js → chunk-4JSR7YO7.js} +20 -1
  14. package/dist/{chunk-N432I7QH.js → chunk-6OWJXUAR.js} +1 -1
  15. package/dist/{chunk-I5KY25PQ.js → chunk-6WAWMWR5.js} +1 -1
  16. package/dist/{chunk-NNB4WIG7.js → chunk-7F2SW2KD.js} +2 -2
  17. package/dist/chunk-7KJOFUNN.js +22 -0
  18. package/dist/{chunk-7J3HEVR7.js → chunk-B2BVAIZ4.js} +15 -9
  19. package/dist/{chunk-VH33ZWMW.js → chunk-BDK73LK6.js} +1 -1
  20. package/dist/{chunk-QTUVYI7W.js → chunk-BFWHBQK4.js} +1 -1
  21. package/dist/{chunk-JYVGHWEJ.js → chunk-BM474GX6.js} +3 -3
  22. package/dist/{chunk-LOEJ4HPQ.js → chunk-BTWAGDV5.js} +1 -1
  23. package/dist/{chunk-A2A4KLFE.js → chunk-CVL5IGIR.js} +596 -40
  24. package/dist/{chunk-RVGLDGMI.js → chunk-E5C7OWZ2.js} +20 -22
  25. package/dist/chunk-FYCALD4Q.js +23 -0
  26. package/dist/{chunk-SKLSMHXO.js → chunk-IS7WJ56Q.js} +1 -1
  27. package/dist/{chunk-2NGTS5UU.js → chunk-M3K5AARV.js} +1 -1
  28. package/dist/{chunk-ALEF47VT.js → chunk-MLOQKQNB.js} +1 -1
  29. package/dist/{chunk-C7I35G4R.js → chunk-N3DNFPVA.js} +41 -5
  30. package/dist/{chunk-LRCG2JLP.js → chunk-N7BLAHNE.js} +5 -1
  31. package/dist/{chunk-UKVWJRKN.js → chunk-PLDWHR4D.js} +1 -1
  32. package/dist/{chunk-3Z2DPESO.js → chunk-TAHX36HZ.js} +126 -81
  33. package/dist/{chunk-KIEPMIM5.js → chunk-U5BTYSAL.js} +1 -1
  34. package/dist/{chunk-GY5HBI7A.js → chunk-V45JXOWY.js} +2 -2
  35. package/dist/{chunk-JUKK7FPS.js → chunk-V6ZCNULL.js} +2 -2
  36. package/dist/{chunk-KVK2DLWI.js → chunk-XWXBJQBE.js} +2 -2
  37. package/dist/cli.js +23 -23
  38. package/dist/{clock-BVH3V6E3.js → clock-3X4DSC2N.js} +40 -25
  39. package/dist/{cloud-sync-4NWLMFVH.js → cloud-sync-TG3TIX5H.js} +21 -21
  40. package/dist/{config-H2H4UIF7.js → config-OROA5DUA.js} +4 -4
  41. package/dist/connectors/discord-bridge.js +1 -1
  42. package/dist/connectors/slack-bridge.js +1 -1
  43. package/dist/connectors/telegram-bridge.js +1 -1
  44. package/dist/{conversations-AWI5SZW2.js → conversations-HL2JP5GI.js} +5 -5
  45. package/dist/{create-YWD2TIP4.js → create-3SEKKI6P.js} +6 -6
  46. package/dist/{create-2FK7Z46Y.js → create-UOSOQ2HN.js} +4 -4
  47. package/dist/daemon-client-WOAQXXBM.js +12 -0
  48. package/dist/{daemon-restart-GOBUKLX7.js → daemon-restart-5ABHNXJZ.js} +9 -9
  49. package/dist/daemon.js +1747 -688
  50. package/dist/{db-RA45JBFG.js → db-PLEDCBHZ.js} +1 -1
  51. package/dist/db-RYX3SS2W.js +9 -0
  52. package/dist/{delete-QTGWEDBI.js → delete-KYOVWR23.js} +3 -3
  53. package/dist/delivery-manager-2BR5NZKF.js +32 -0
  54. package/dist/{delivery-router-FL45JL7N.js → delivery-router-D5ELDMS2.js} +4 -4
  55. package/dist/down-QVFN4UPK.js +15 -0
  56. package/dist/{env-JCOF2222.js → env-R34DT7XL.js} +12 -8
  57. package/dist/exec-DVLXKRIO.js +17 -0
  58. package/dist/{export-SUYRLI5Q.js → export-6ZXAXATG.js} +6 -6
  59. package/dist/extension-PM42QCID.js +97 -0
  60. package/dist/extensions-BBGVL5JC.js +38 -0
  61. package/dist/{files-65PMW5IK.js → files-VQV2VZQO.js} +7 -7
  62. package/dist/{import-DDUFE7AY.js → import-MK2I2T6F.js} +5 -5
  63. package/dist/{isolation-LLAYQYDY.js → isolation-62MKDZN3.js} +4 -4
  64. package/dist/{join-I5QEE3LG.js → join-DGYHTJUH.js} +3 -3
  65. package/dist/lib-DYEZMGW7.js +6588 -0
  66. package/dist/{list-JQ463EDA.js → list-C644WTHV.js} +18 -10
  67. package/dist/{login-D7ETSU4R.js → login-IIGEQPHL.js} +6 -6
  68. package/dist/{login-RIJF2F4G.js → login-KZQLMAWE.js} +4 -4
  69. package/dist/{logout-5MLHZALK.js → logout-AGTZVRGP.js} +4 -4
  70. package/dist/{logout-UZJRGY4Z.js → logout-KD6GXIJJ.js} +4 -4
  71. package/dist/message-delivery-V3R6NXJP.js +42 -0
  72. package/dist/{mind-IOJFLEM5.js → mind-BI4EPBVZ.js} +19 -19
  73. package/dist/{mind-activity-tracker-F6O4Q2SL.js → mind-activity-tracker-2ACNHA7B.js} +5 -5
  74. package/dist/mind-history-WOYFLQAI.js +264 -0
  75. package/dist/{mind-list-WUPMQDYQ.js → mind-list-6VPM7GUQ.js} +4 -4
  76. package/dist/mind-manager-MWW3BTS4.js +32 -0
  77. package/dist/{mind-profile-P67FEHOY.js → mind-profile-WPG42U5Y.js} +2 -2
  78. package/dist/mind-service-VIKZJK2M.js +38 -0
  79. package/dist/{mind-sleep-WW2IX7JT.js → mind-sleep-XDISJY74.js} +6 -6
  80. package/dist/{mind-status-L3EFFRPR.js → mind-status-7FTZWPZF.js} +4 -4
  81. package/dist/{mind-wake-VSSGW465.js → mind-wake-KIIKEI3A.js} +6 -6
  82. package/dist/{package-U3VFO273.js → package-V2WHWVG6.js} +8 -5
  83. package/dist/{read-EBY56C33.js → read-H5C26YO7.js} +20 -10
  84. package/dist/{read-stdin-HQJ7774D.js → read-stdin-PIRM6A2Y.js} +1 -1
  85. package/dist/{register-HD74C4TT.js → register-J27WP33N.js} +6 -6
  86. package/dist/{registry-PJ4S5PHQ.js → registry-UYV5S6QT.js} +3 -3
  87. package/dist/{reject-UJKFBHRO.js → reject-OEANJYIA.js} +6 -6
  88. package/dist/{restart-3UCMRUVC.js → restart-V5EGYBJG.js} +4 -4
  89. package/dist/{sandbox-GJOK4QLQ.js → sandbox-SI5HMBP3.js} +5 -5
  90. package/dist/scheduler-AGG3L2FO.js +32 -0
  91. package/dist/{schema-PA3M5ZKH.js → schema-ETMABTW4.js} +4 -2
  92. package/dist/{seed-QDYVLG74.js → seed-WNGI6PNW.js} +2 -2
  93. package/dist/{seed-check-S2IX25RL.js → seed-check-PXTH7YXS.js} +2 -2
  94. package/dist/{seed-cmd-DKOUFEAU.js → seed-cmd-VENFTGS3.js} +4 -4
  95. package/dist/{seed-create-4XBBOLRH.js → seed-create-663ALOKH.js} +6 -6
  96. package/dist/{seed-sprout-GQEIIQRT.js → seed-sprout-EH3AGKAI.js} +12 -12
  97. package/dist/{send-QIV2INHB.js → send-7FUUUZZH.js} +23 -10
  98. package/dist/{setup-TISPCO22.js → setup-GGMKENLN.js} +4 -4
  99. package/dist/{setup-XMCBE3LF.js → setup-Z3DEVWV7.js} +11 -11
  100. package/dist/{skill-PSQGRRJX.js → skill-DKNYJS4P.js} +14 -10
  101. package/dist/skills/plan-coordinator/SKILL.md +60 -0
  102. package/dist/skills/volute-mind/SKILL.md +7 -221
  103. package/dist/skills/volute-mind/references/extensions.md +37 -0
  104. package/dist/skills/volute-mind/references/integrations.md +48 -0
  105. package/dist/skills/volute-mind/references/routing.md +86 -0
  106. package/dist/skills/volute-mind/references/sleep.md +33 -0
  107. package/dist/skills/volute-mind/references/variants.md +31 -0
  108. package/dist/{skills-7FV7EJTE.js → skills-Q6VZ2UGD.js} +11 -7
  109. package/dist/sleep-manager-BJK2ROPX.js +36 -0
  110. package/dist/spirit-4JP4TY4C.js +23 -0
  111. package/dist/{split-STOROBYJ.js → split-3YPMS2CL.js} +3 -3
  112. package/dist/{sprout-WKLZXUIQ.js → sprout-E3HJIV2Z.js} +2 -2
  113. package/dist/{start-K2NCUUCG.js → start-W3TPKX4D.js} +4 -4
  114. package/dist/{status-3JBTFSMI.js → status-4OVFXFEJ.js} +7 -7
  115. package/dist/{stop-H26JZDXF.js → stop-GTT6YWYO.js} +4 -4
  116. package/dist/system-channel-DXD2JBOU.js +36 -0
  117. package/dist/system-chat-TYLOL7SX.js +36 -0
  118. package/dist/{systems-XRI52VCH.js → systems-AYLO727G.js} +7 -7
  119. package/dist/{tailscale-XHQBZROW.js → tailscale-ZEUK7GKZ.js} +3 -3
  120. package/dist/{template-hash-A6VVKOXJ.js → template-hash-EJRTKE36.js} +1 -1
  121. package/dist/up-PA7F2CXE.js +18 -0
  122. package/dist/{update-UD543CXX.js → update-HG4LCUSG.js} +7 -7
  123. package/dist/{update-check-ZD6OOIYQ.js → update-check-X3YG4WVP.js} +4 -4
  124. package/dist/{upgrade-O4Q7WJM3.js → upgrade-YGNIDICG.js} +3 -3
  125. package/dist/{variant-7TGZHOU3.js → variant-MZUMRTQO.js} +1 -1
  126. package/dist/{version-notify-NBI2MTJO.js → version-notify-YCH4UVQ2.js} +19 -19
  127. package/dist/{volute-config-HD7WWUQC.js → volute-config-WBKYJGYQ.js} +1 -1
  128. package/dist/web-assets/assets/index-DiiwC-CZ.css +1 -0
  129. package/dist/web-assets/assets/index-d6y5b9Ij.js +75 -0
  130. package/dist/web-assets/ext-theme.css +48 -9
  131. package/dist/web-assets/index.html +2 -2
  132. package/drizzle/0005_meta_summaries.sql +15 -0
  133. package/drizzle/meta/0005_snapshot.json +7 -0
  134. package/drizzle/meta/_journal.json +7 -0
  135. package/package.json +7 -4
  136. package/packages/extensions/plan/dist/ui/assets/index-CJj2gZnZ.css +1 -0
  137. package/packages/extensions/plan/dist/ui/assets/index-FMEJmvQz.js +61 -0
  138. package/packages/extensions/plan/dist/ui/index.html +14 -0
  139. package/packages/extensions/plan/skills/plan/SKILL.md +43 -0
  140. package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +37 -0
  141. package/templates/_base/home/VOLUTE.md +12 -19
  142. package/templates/_base/src/lib/context-breakdown.ts +450 -0
  143. package/templates/_base/src/lib/format-prefix.ts +17 -0
  144. package/templates/_base/src/lib/hook-loader.ts +8 -2
  145. package/templates/_base/src/lib/router.ts +75 -33
  146. package/templates/_base/src/lib/routing.ts +4 -1
  147. package/templates/_base/src/lib/startup.ts +16 -8
  148. package/templates/_base/src/lib/types.ts +2 -1
  149. package/templates/_base/src/lib/volute-server.ts +69 -8
  150. package/templates/claude/.init/CLAUDE.md +4 -10
  151. package/templates/claude/package.json.tmpl +1 -0
  152. package/templates/claude/src/agent.ts +100 -32
  153. package/templates/claude/src/lib/hooks/reply-instructions.ts +27 -7
  154. package/templates/claude/src/lib/stream-consumer.ts +2 -2
  155. package/templates/claude/src/server.ts +1 -0
  156. package/templates/codex/package.json.tmpl +1 -0
  157. package/templates/codex/src/agent.ts +80 -8
  158. package/templates/codex/src/server.ts +1 -4
  159. package/templates/pi/package.json.tmpl +1 -0
  160. package/templates/pi/src/agent.ts +115 -36
  161. package/templates/pi/src/lib/event-handler.ts +22 -7
  162. package/templates/pi/src/lib/reply-instructions-extension.ts +23 -4
  163. package/templates/pi/src/lib/subagents.ts +20 -17
  164. package/templates/pi/src/server.ts +2 -5
  165. package/dist/chunk-K3NQKI34.js +0 -10
  166. package/dist/daemon-client-6QXHZ7US.js +0 -12
  167. package/dist/db-F34YLV7D.js +0 -9
  168. package/dist/delivery-manager-PFAKEJTC.js +0 -32
  169. package/dist/down-FWWTEKXM.js +0 -15
  170. package/dist/extension-OBTGKQQD.js +0 -175
  171. package/dist/extensions-KYNTVTMO.js +0 -30
  172. package/dist/history-DKCDI3JO.js +0 -128
  173. package/dist/message-delivery-DFF5SJRM.js +0 -42
  174. package/dist/mind-manager-NBJF5D26.js +0 -32
  175. package/dist/mind-service-2MQ6UK5N.js +0 -38
  176. package/dist/scheduler-ZZ7XGQG6.js +0 -32
  177. package/dist/sleep-manager-JTXSN7NV.js +0 -36
  178. package/dist/spirit-VRONKFMF.js +0 -23
  179. package/dist/system-chat-JAPOJ3KE.js +0 -36
  180. package/dist/up-M5AS6SBV.js +0 -18
  181. package/dist/web-assets/assets/index-CWJrVveV.css +0 -1
  182. package/dist/web-assets/assets/index-DJt14FRI.js +0 -75
@@ -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,6 +450,9 @@ 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
 
@@ -455,9 +481,17 @@ export function createMind(options: {
455
481
  };
456
482
  session.listeners.add(filteredListener);
457
483
 
458
- // Track channel for reply instructions
484
+ // Track channel/sender for reply instructions
459
485
  if (meta.channel) {
460
- session.messageChannels.set(meta.messageId, meta.channel);
486
+ session.messageChannels.set(meta.messageId, {
487
+ channel: meta.channel,
488
+ sender: meta.sender,
489
+ });
490
+ }
491
+
492
+ // Update reply instructions mode from routing config
493
+ if (meta.replyInstructions) {
494
+ session.replyInstructionsMode = meta.replyInstructions;
461
495
  }
462
496
 
463
497
  // Interrupt if requested and session is mid-turn
@@ -497,5 +531,39 @@ export function createMind(options: {
497
531
  return handler;
498
532
  }
499
533
 
500
- return { resolve, waitForCommits: autoCommit.waitForCommits };
534
+ const systemPromptTokens = countSystemPromptTokens(options.systemPrompt);
535
+ const claudeMdTokens = countSdkInstructionTokens(options.cwd);
536
+ const skillDescTokens = countSkillDescriptionTokens([resolvePath(options.cwd, ".claude/skills")]);
537
+
538
+ function getContextInfo(): ContextInfo {
539
+ return {
540
+ sessions: Array.from(sessions.values()).map((s) => {
541
+ try {
542
+ const sessionId = sessionStore.load(s.name);
543
+ const jsonlPath = sessionId ? findClaudeSessionFile(options.cwd, sessionId) : null;
544
+ const parsed = jsonlPath
545
+ ? parseClaudeSessionJSONL(
546
+ jsonlPath,
547
+ systemPromptTokens,
548
+ claudeMdTokens,
549
+ skillDescTokens,
550
+ )
551
+ : null;
552
+
553
+ return {
554
+ name: s.name,
555
+ contextTokens: parsed?.contextTokens ?? s.contextTokens,
556
+ contextWindow: maxContextTokens,
557
+ breakdown: parsed?.breakdown,
558
+ };
559
+ } catch (err) {
560
+ log("mind", `failed to get context breakdown for session "${s.name}":`, err);
561
+ return { name: s.name, contextTokens: s.contextTokens, contextWindow: maxContextTokens };
562
+ }
563
+ }),
564
+ systemPrompt: systemPromptTokens,
565
+ };
566
+ }
567
+
568
+ return { resolve, waitForCommits: autoCommit.waitForCommits, getContextInfo };
501
569
  }
@@ -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"