volute 0.32.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 (187) hide show
  1. package/README.md +16 -0
  2. package/dist/{accept-74M7I4RZ.js → accept-TW6V4WI4.js} +4 -4
  3. package/dist/{activity-events-HETAODOK.js → activity-events-BN7V6KCC.js} +4 -4
  4. package/dist/{ai-service-ZIPCV3MX.js → ai-service-PSILB5WD.js} +5 -5
  5. package/dist/{api-client-YPKOZP2O.js → api-client-XUXOB7LI.js} +1 -1
  6. package/dist/api.d.ts +1198 -957
  7. package/dist/{archive-INXYFVCW.js → archive-C2VEMQOR.js} +4 -4
  8. package/dist/{auth-6DMGES3I.js → auth-ZFZXJZDQ.js} +5 -5
  9. package/dist/{bridge-BVCBTGPF.js → bridge-O753D5F4.js} +4 -4
  10. package/dist/{chat-XT4OBJBU.js → chat-BHYX7DJ4.js} +9 -9
  11. package/dist/{chunk-M7UL5S3Q.js → chunk-2IOP6PHB.js} +1 -1
  12. package/dist/{chunk-JJ7W6WSB.js → chunk-47XDEWWV.js} +5 -5
  13. package/dist/{chunk-RSX4OPZY.js → chunk-47ZPNLF4.js} +7 -7
  14. package/dist/{chunk-RPZZSXV3.js → chunk-4JSR7YO7.js} +20 -1
  15. package/dist/chunk-6LXAAQ43.js +22 -0
  16. package/dist/{chunk-TSXLLQZW.js → chunk-6OWJXUAR.js} +10 -1
  17. package/dist/{chunk-I5KY25PQ.js → chunk-6WAWMWR5.js} +1 -1
  18. package/dist/{chunk-LSGWR54X.js → chunk-7F2SW2KD.js} +2 -2
  19. package/dist/chunk-7KJOFUNN.js +22 -0
  20. package/dist/{spirit-N4W4UQRH.js → chunk-B2BVAIZ4.js} +21 -12
  21. package/dist/{chunk-LGB6JBHI.js → chunk-BDK73LK6.js} +5 -55
  22. package/dist/{chunk-IYDIE3HG.js → chunk-BFWHBQK4.js} +1 -1
  23. package/dist/{chunk-TDRYEPH4.js → chunk-BM474GX6.js} +4 -4
  24. package/dist/{chunk-R7E6CRVQ.js → chunk-BTWAGDV5.js} +1 -1
  25. package/dist/{chunk-WKF5FEFK.js → chunk-CVL5IGIR.js} +629 -174
  26. package/dist/{chunk-S6NFERDC.js → chunk-E5C7OWZ2.js} +20 -22
  27. package/dist/chunk-FYCALD4Q.js +23 -0
  28. package/dist/{chunk-SKLSMHXO.js → chunk-IS7WJ56Q.js} +1 -1
  29. package/dist/{chunk-2NGTS5UU.js → chunk-M3K5AARV.js} +1 -1
  30. package/dist/{chunk-ALEF47VT.js → chunk-MLOQKQNB.js} +1 -1
  31. package/dist/{chunk-D5G5YOPL.js → chunk-N3DNFPVA.js} +41 -5
  32. package/dist/{chunk-LRCG2JLP.js → chunk-N7BLAHNE.js} +5 -1
  33. package/dist/chunk-OYAKCAVY.js +29 -0
  34. package/dist/{chunk-UKVWJRKN.js → chunk-PLDWHR4D.js} +1 -1
  35. package/dist/{chunk-QBQ424EM.js → chunk-TAHX36HZ.js} +545 -246
  36. package/dist/chunk-U5BTYSAL.js +59 -0
  37. package/dist/{chunk-SX5TKJBZ.js → chunk-V45JXOWY.js} +2 -2
  38. package/dist/{chunk-2FLJ63GU.js → chunk-V6ZCNULL.js} +2 -2
  39. package/dist/{chunk-QZANELPX.js → chunk-XWXBJQBE.js} +3 -2
  40. package/dist/cli.js +32 -24
  41. package/dist/{clock-2UOZ6JPU.js → clock-3X4DSC2N.js} +38 -23
  42. package/dist/{cloud-sync-JN3NWKEM.js → cloud-sync-TG3TIX5H.js} +21 -17
  43. package/dist/{config-H2H4UIF7.js → config-OROA5DUA.js} +4 -4
  44. package/dist/connectors/discord-bridge.js +1 -1
  45. package/dist/connectors/slack-bridge.js +1 -1
  46. package/dist/connectors/telegram-bridge.js +1 -1
  47. package/dist/{conversations-3O5O6AS3.js → conversations-HL2JP5GI.js} +5 -5
  48. package/dist/{create-RNLNCORE.js → create-3SEKKI6P.js} +5 -5
  49. package/dist/{create-WBBYI6V7.js → create-UOSOQ2HN.js} +4 -4
  50. package/dist/daemon-client-WOAQXXBM.js +12 -0
  51. package/dist/{daemon-restart-NGFHFAUF.js → daemon-restart-5ABHNXJZ.js} +9 -8
  52. package/dist/daemon.js +2730 -1520
  53. package/dist/{db-RA45JBFG.js → db-PLEDCBHZ.js} +1 -1
  54. package/dist/db-RYX3SS2W.js +9 -0
  55. package/dist/{delete-QTGWEDBI.js → delete-KYOVWR23.js} +3 -3
  56. package/dist/delivery-manager-2BR5NZKF.js +32 -0
  57. package/dist/{delivery-router-FL45JL7N.js → delivery-router-D5ELDMS2.js} +4 -4
  58. package/dist/down-QVFN4UPK.js +15 -0
  59. package/dist/{env-RLYQBOOP.js → env-R34DT7XL.js} +10 -6
  60. package/dist/exec-DVLXKRIO.js +17 -0
  61. package/dist/{export-SUYRLI5Q.js → export-6ZXAXATG.js} +6 -6
  62. package/dist/extension-PM42QCID.js +97 -0
  63. package/dist/extensions-BBGVL5JC.js +38 -0
  64. package/dist/{files-EAMPO2SJ.js → files-VQV2VZQO.js} +5 -5
  65. package/dist/{import-DDUFE7AY.js → import-MK2I2T6F.js} +5 -5
  66. package/dist/isolation-62MKDZN3.js +22 -0
  67. package/dist/{join-I5QEE3LG.js → join-DGYHTJUH.js} +3 -3
  68. package/dist/lib-DYEZMGW7.js +6588 -0
  69. package/dist/{list-DW2VRTOZ.js → list-C644WTHV.js} +16 -8
  70. package/dist/{login-7CHPW2PN.js → login-IIGEQPHL.js} +4 -4
  71. package/dist/{login-RIJF2F4G.js → login-KZQLMAWE.js} +4 -4
  72. package/dist/{logout-5MLHZALK.js → logout-AGTZVRGP.js} +4 -4
  73. package/dist/{logout-UZJRGY4Z.js → logout-KD6GXIJJ.js} +4 -4
  74. package/dist/message-delivery-V3R6NXJP.js +42 -0
  75. package/dist/{mind-2B6M7Y25.js → mind-BI4EPBVZ.js} +25 -19
  76. package/dist/{mind-activity-tracker-NZZT2NTT.js → mind-activity-tracker-2ACNHA7B.js} +5 -5
  77. package/dist/mind-history-WOYFLQAI.js +264 -0
  78. package/dist/{mind-list-WUPMQDYQ.js → mind-list-6VPM7GUQ.js} +4 -4
  79. package/dist/mind-manager-MWW3BTS4.js +32 -0
  80. package/dist/mind-profile-WPG42U5Y.js +47 -0
  81. package/dist/mind-service-VIKZJK2M.js +38 -0
  82. package/dist/{mind-sleep-B7BHJLH7.js → mind-sleep-XDISJY74.js} +4 -4
  83. package/dist/{mind-status-L3EFFRPR.js → mind-status-7FTZWPZF.js} +4 -4
  84. package/dist/{mind-wake-GY3RFX7Y.js → mind-wake-KIIKEI3A.js} +4 -4
  85. package/dist/{package-PK6JUFL3.js → package-V2WHWVG6.js} +9 -5
  86. package/dist/{read-5AMJRO3D.js → read-H5C26YO7.js} +18 -8
  87. package/dist/read-stdin-PIRM6A2Y.js +8 -0
  88. package/dist/{register-V2JZZKFK.js → register-J27WP33N.js} +4 -4
  89. package/dist/{registry-PJ4S5PHQ.js → registry-UYV5S6QT.js} +3 -3
  90. package/dist/{reject-33HEZMZ4.js → reject-OEANJYIA.js} +4 -4
  91. package/dist/{restart-3UCMRUVC.js → restart-V5EGYBJG.js} +4 -4
  92. package/dist/{sandbox-JANNTX6U.js → sandbox-SI5HMBP3.js} +5 -5
  93. package/dist/scheduler-AGG3L2FO.js +32 -0
  94. package/dist/{schema-PA3M5ZKH.js → schema-ETMABTW4.js} +4 -2
  95. package/dist/seed-WNGI6PNW.js +11 -0
  96. package/dist/seed-check-PXTH7YXS.js +32 -0
  97. package/dist/seed-cmd-VENFTGS3.js +36 -0
  98. package/dist/{seed-ALUQ55FF.js → seed-create-663ALOKH.js} +8 -8
  99. package/dist/{sprout-L2GFOVF7.js → seed-sprout-EH3AGKAI.js} +24 -11
  100. package/dist/{send-3MI36LEF.js → send-7FUUUZZH.js} +66 -51
  101. package/dist/{setup-SZIARWI6.js → setup-GGMKENLN.js} +6 -4
  102. package/dist/{setup-WENLVPVP.js → setup-Z3DEVWV7.js} +13 -11
  103. package/dist/{skill-TUVOTW4Z.js → skill-DKNYJS4P.js} +12 -8
  104. package/dist/skills/imagegen/SKILL.md +11 -7
  105. package/dist/skills/imagegen/scripts/imagegen.ts +146 -25
  106. package/dist/skills/orientation/SKILL.md +9 -2
  107. package/dist/skills/plan-coordinator/SKILL.md +60 -0
  108. package/dist/skills/seed-nurture/SKILL.md +42 -0
  109. package/dist/skills/volute-mind/SKILL.md +11 -221
  110. package/dist/skills/volute-mind/references/extensions.md +37 -0
  111. package/dist/skills/volute-mind/references/integrations.md +48 -0
  112. package/dist/skills/volute-mind/references/routing.md +86 -0
  113. package/dist/skills/volute-mind/references/sleep.md +33 -0
  114. package/dist/skills/volute-mind/references/variants.md +31 -0
  115. package/dist/{skills-XNZK6P4K.js → skills-Q6VZ2UGD.js} +11 -6
  116. package/dist/sleep-manager-BJK2ROPX.js +36 -0
  117. package/dist/spirit-4JP4TY4C.js +23 -0
  118. package/dist/{split-STOROBYJ.js → split-3YPMS2CL.js} +3 -3
  119. package/dist/sprout-E3HJIV2Z.js +11 -0
  120. package/dist/{start-K2NCUUCG.js → start-W3TPKX4D.js} +4 -4
  121. package/dist/{status-TCUMUO6M.js → status-4OVFXFEJ.js} +7 -6
  122. package/dist/{stop-H26JZDXF.js → stop-GTT6YWYO.js} +4 -4
  123. package/dist/system-channel-DXD2JBOU.js +36 -0
  124. package/dist/system-chat-TYLOL7SX.js +36 -0
  125. package/dist/{systems-DHBKVYEY.js → systems-AYLO727G.js} +7 -7
  126. package/dist/{tailscale-XHQBZROW.js → tailscale-ZEUK7GKZ.js} +3 -3
  127. package/dist/{template-hash-A6VVKOXJ.js → template-hash-EJRTKE36.js} +1 -1
  128. package/dist/up-PA7F2CXE.js +18 -0
  129. package/dist/{update-QVPRF6GR.js → update-HG4LCUSG.js} +7 -6
  130. package/dist/{update-check-ZD6OOIYQ.js → update-check-X3YG4WVP.js} +4 -4
  131. package/dist/{upgrade-O4Q7WJM3.js → upgrade-YGNIDICG.js} +3 -3
  132. package/dist/{variant-7TGZHOU3.js → variant-MZUMRTQO.js} +1 -1
  133. package/dist/{version-notify-TCKWBZZG.js → version-notify-YCH4UVQ2.js} +23 -20
  134. package/dist/volute-config-WBKYJGYQ.js +10 -0
  135. package/dist/web-assets/assets/index-DiiwC-CZ.css +1 -0
  136. package/dist/web-assets/assets/index-d6y5b9Ij.js +75 -0
  137. package/dist/web-assets/ext-theme.css +48 -9
  138. package/dist/web-assets/index.html +2 -2
  139. package/drizzle/0005_meta_summaries.sql +15 -0
  140. package/drizzle/meta/0005_snapshot.json +7 -0
  141. package/drizzle/meta/_journal.json +7 -0
  142. package/package.json +8 -4
  143. package/packages/extensions/plan/dist/ui/assets/index-CJj2gZnZ.css +1 -0
  144. package/packages/extensions/plan/dist/ui/assets/index-FMEJmvQz.js +61 -0
  145. package/packages/extensions/plan/dist/ui/index.html +14 -0
  146. package/packages/extensions/plan/skills/plan/SKILL.md +43 -0
  147. package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +37 -0
  148. package/templates/_base/home/VOLUTE.md +12 -19
  149. package/templates/_base/src/lib/context-breakdown.ts +450 -0
  150. package/templates/_base/src/lib/format-prefix.ts +17 -0
  151. package/templates/_base/src/lib/hook-loader.ts +8 -2
  152. package/templates/_base/src/lib/router.ts +75 -33
  153. package/templates/_base/src/lib/routing.ts +4 -1
  154. package/templates/_base/src/lib/startup.ts +16 -8
  155. package/templates/_base/src/lib/types.ts +2 -1
  156. package/templates/_base/src/lib/volute-server.ts +69 -8
  157. package/templates/claude/.init/CLAUDE.md +4 -10
  158. package/templates/claude/package.json.tmpl +1 -0
  159. package/templates/claude/src/agent.ts +100 -32
  160. package/templates/claude/src/lib/hooks/reply-instructions.ts +27 -7
  161. package/templates/claude/src/lib/stream-consumer.ts +40 -2
  162. package/templates/claude/src/server.ts +1 -0
  163. package/templates/codex/package.json.tmpl +1 -0
  164. package/templates/codex/src/agent.ts +81 -8
  165. package/templates/codex/src/server.ts +1 -4
  166. package/templates/pi/package.json.tmpl +1 -0
  167. package/templates/pi/src/agent.ts +115 -36
  168. package/templates/pi/src/lib/event-handler.ts +22 -7
  169. package/templates/pi/src/lib/reply-instructions-extension.ts +23 -4
  170. package/templates/pi/src/lib/subagents.ts +20 -17
  171. package/templates/pi/src/server.ts +2 -5
  172. package/dist/chunk-K3NQKI34.js +0 -10
  173. package/dist/daemon-client-6QXHZ7US.js +0 -12
  174. package/dist/db-F34YLV7D.js +0 -9
  175. package/dist/delivery-manager-SDVXFD4W.js +0 -28
  176. package/dist/down-TB3ESMNP.js +0 -14
  177. package/dist/extension-FQ5D3NCC.js +0 -174
  178. package/dist/extensions-GDYWQXC4.js +0 -29
  179. package/dist/history-FO5PHBQ5.js +0 -128
  180. package/dist/message-delivery-2FIM7QKO.js +0 -32
  181. package/dist/mind-manager-BNCMGYXW.js +0 -28
  182. package/dist/mind-service-AV273WT4.js +0 -34
  183. package/dist/sleep-manager-53DZOWW7.js +0 -32
  184. package/dist/system-chat-NPYFYZVI.js +0 -32
  185. package/dist/up-6I6BHRTO.js +0 -17
  186. package/dist/web-assets/assets/index-Bui7U9Uu.css +0 -1
  187. package/dist/web-assets/assets/index-e36DIo1b.js +0 -73
@@ -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,
@@ -74,6 +74,44 @@ export async function consumeStream(
74
74
  }
75
75
  }
76
76
  }
77
+ if (msg.type === "user") {
78
+ // Tool result messages — the SDK sends these after tool execution.
79
+ // Extract tool_result content blocks and emit them so the daemon can
80
+ // link outbound records to the correct turn via correlation markers.
81
+ const content = (msg as { message?: { content?: unknown[] } }).message?.content;
82
+ if (Array.isArray(content)) {
83
+ for (const b of content) {
84
+ if (
85
+ b &&
86
+ typeof b === "object" &&
87
+ "type" in b &&
88
+ b.type === "tool_result" &&
89
+ "content" in b
90
+ ) {
91
+ const resultContent = Array.isArray(b.content)
92
+ ? b.content
93
+ .filter(
94
+ (c: unknown): c is { type: "text"; text: string } =>
95
+ !!c && typeof c === "object" && "type" in c && c.type === "text",
96
+ )
97
+ .map((c) => c.text)
98
+ .join("")
99
+ : typeof b.content === "string"
100
+ ? b.content
101
+ : "";
102
+ if (resultContent) {
103
+ const toolUseId =
104
+ "tool_use_id" in b && typeof b.tool_use_id === "string" ? b.tool_use_id : "unknown";
105
+ emit(session, {
106
+ type: "tool_result",
107
+ content: resultContent,
108
+ metadata: { tool_use_id: toolUseId },
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
77
115
  if (msg.type === "result") {
78
116
  if (session.currentMessageId) {
79
117
  session.messageChannels.delete(session.currentMessageId);
@@ -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 {
@@ -134,6 +149,7 @@ export function createMind(options: {
134
149
  function initSession(session: CodexSession) {
135
150
  const isEphemeral = session.name.startsWith("new-");
136
151
  log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
152
+ emit(session, { type: "session_start" });
137
153
 
138
154
  if (!isEphemeral) {
139
155
  const savedThreadId = sessionStore.load(session.name);
@@ -143,6 +159,7 @@ export function createMind(options: {
143
159
  session.thread = codex.resumeThread(savedThreadId, {
144
160
  workingDirectory: options.cwd,
145
161
  model: options.model,
162
+ modelReasoningEffort: options.reasoningEffort,
146
163
  skipGitRepoCheck: true,
147
164
  sandboxMode: "danger-full-access",
148
165
  });
@@ -157,6 +174,7 @@ export function createMind(options: {
157
174
  session.thread = codex.startThread({
158
175
  workingDirectory: options.cwd,
159
176
  model: options.model,
177
+ modelReasoningEffort: options.reasoningEffort,
160
178
  skipGitRepoCheck: true,
161
179
  sandboxMode: "danger-full-access",
162
180
  });
@@ -192,6 +210,20 @@ export function createMind(options: {
192
210
  // Refresh system prompt before each turn (picks up MEMORY.md changes)
193
211
  refreshSystemPrompt();
194
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
+
195
227
  // Run pre-prompt hooks
196
228
  try {
197
229
  const hookResult = await runHooks(hooksDir, "pre-prompt", {
@@ -210,11 +242,14 @@ export function createMind(options: {
210
242
  warn("mind", "pre-prompt hook failed:", err);
211
243
  }
212
244
 
213
- // Reply instructions on first message per channel
245
+ // Reply instructions on first message per channel (skip system messages)
214
246
  const channel = meta.channel;
215
247
  if (channel && !session.firstMessagePerChannel.has(channel)) {
216
248
  session.firstMessagePerChannel.add(channel);
217
- 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);
218
253
  emit(session, {
219
254
  type: "context",
220
255
  content: replyInstructions,
@@ -280,7 +315,9 @@ export function createMind(options: {
280
315
  if (item.type === "agent_message" || item.type === "agentMessage") {
281
316
  itemText.set(event.itemId ?? item.id, "");
282
317
  } else if (item.type === "reasoning") {
283
- 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 });
284
321
  } else if (item.type === "command_execution" || item.type === "commandExecution") {
285
322
  const cmd = item.command ?? item.args?.join(" ") ?? "";
286
323
  emit(session, {
@@ -355,7 +392,10 @@ export function createMind(options: {
355
392
  if (!item) break;
356
393
  const itemType = item.type;
357
394
 
358
- 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") {
359
399
  // Emit any remaining delta
360
400
  const id = event.itemId ?? item.id;
361
401
  const prev = itemText.get(id) ?? "";
@@ -549,5 +589,38 @@ export function createMind(options: {
549
589
  return handler;
550
590
  }
551
591
 
552
- 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 };
553
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"