volute 0.20.0 → 0.22.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 (114) hide show
  1. package/README.md +7 -7
  2. package/dist/{activity-events-OMXKXD5N.js → activity-events-3WHHCOBB.js} +3 -4
  3. package/dist/api.d.ts +4294 -0
  4. package/dist/{archive-ZCFOSTKB.js → archive-4ZQYK5MN.js} +4 -2
  5. package/dist/auth-HM2RSPY7.js +37 -0
  6. package/dist/{channel-PUQKGSQM.js → channel-BOOMFULW.js} +2 -2
  7. package/dist/{chunk-UU7A7KLB.js → chunk-A4S7H6G6.js} +5 -7
  8. package/dist/chunk-AKPFNL7L.js +148 -0
  9. package/dist/{chunk-EBGCNDMM.js → chunk-B2CPS4QU.js} +128 -114
  10. package/dist/chunk-G5KRTU2F.js +76 -0
  11. package/dist/{chunk-FCDU5BFX.js → chunk-HFCBO2GL.js} +2 -2
  12. package/dist/{chunk-GZ7DW4YL.js → chunk-HGCDWKSP.js} +2 -2
  13. package/dist/{chunk-7UFKREVW.js → chunk-JNFRY2WU.js} +2 -2
  14. package/dist/{chunk-DYZGP3EW.js → chunk-JTDFJWI2.js} +2 -1
  15. package/dist/{chunk-WC6ZHVRL.js → chunk-KFI7TQJ6.js} +2 -2
  16. package/dist/{chunk-AW7P4EVV.js → chunk-KTJGZ7M7.js} +55 -7
  17. package/dist/{chunk-OGXOMR65.js → chunk-NWPT4ASZ.js} +1 -1
  18. package/dist/{chunk-SCUDS4US.js → chunk-ON3FF5JA.js} +1 -1
  19. package/dist/chunk-OSFGKF2T.js +2651 -0
  20. package/dist/{chunk-TIWH32HP.js → chunk-PHHKNGA3.js} +3 -3
  21. package/dist/{chunk-VDWCHYTS.js → chunk-PHU4DEAJ.js} +1 -1
  22. package/dist/{chunk-7NO7EV5Z.js → chunk-QIXPN3OO.js} +2 -2
  23. package/dist/{chunk-O6ASDHFO.js → chunk-RK627D57.js} +40 -63
  24. package/dist/{chunk-NSE7VJQA.js → chunk-SGPEZ32F.js} +29 -1
  25. package/dist/{chunk-IKMY5X76.js → chunk-TFS25FIM.js} +12 -9
  26. package/dist/{chunk-PUVXOZ6T.js → chunk-VNVCRVYI.js} +118 -69
  27. package/dist/{chunk-32VR2EOH.js → chunk-VT5QODNE.js} +2 -2
  28. package/dist/{chunk-RHEGSQFJ.js → chunk-WSLPZF72.js} +1 -1
  29. package/dist/chunk-XLC342FO.js +29 -0
  30. package/dist/cli.js +57 -119
  31. package/dist/cloud-sync-C6WRYRVR.js +96 -0
  32. package/dist/{connector-JBVNZ7VK.js → connector-PYT5UOTZ.js} +6 -6
  33. package/dist/connectors/discord.js +2 -2
  34. package/dist/connectors/slack.js +2 -2
  35. package/dist/connectors/telegram.js +2 -2
  36. package/dist/{create-HP4OVVHF.js → create-WIDA3M4C.js} +1 -1
  37. package/dist/{daemon-client-ITWUCNFO.js → daemon-client-ZHCDL4RS.js} +2 -2
  38. package/dist/{daemon-restart-KPSWNYTH.js → daemon-restart-TPQ2XBRZ.js} +6 -6
  39. package/dist/daemon.js +2250 -1985
  40. package/dist/{delete-BSU7K3RY.js → delete-LOIANQGD.js} +1 -1
  41. package/dist/down-WSUASL5E.js +14 -0
  42. package/dist/{env-A3LMO777.js → env-4PHIHTF4.js} +2 -2
  43. package/dist/{export-6QBUOQGC.js → export-XD6PJBQP.js} +19 -8
  44. package/dist/{file-C57SK5DK.js → file-X4L5TTOL.js} +2 -2
  45. package/dist/{history-WNK3DFUM.js → history-HTEKRNID.js} +2 -2
  46. package/dist/{import-XEC34Y4Z.js → import-EAXTHHXL.js} +4 -3
  47. package/dist/{log-PPPZDVEF.js → log-SRO5Q6AD.js} +2 -2
  48. package/dist/{login-HNH3EUQV.js → login-UO6AOVEA.js} +4 -4
  49. package/dist/{logout-I5CB5UZS.js → logout-UKD5LA37.js} +2 -2
  50. package/dist/{logs-SF2IMJN4.js → logs-HNTNNBDW.js} +2 -2
  51. package/dist/{merge-33C237A4.js → merge-B6SYTGI7.js} +2 -2
  52. package/dist/message-delivery-WUS4K4ZC.js +21 -0
  53. package/dist/{mind-Z7CKD6DG.js → mind-BTXR5B3C.js} +35 -11
  54. package/dist/{mind-activity-tracker-624QLQLC.js → mind-activity-tracker-PGC3DBJ7.js} +4 -5
  55. package/dist/{mind-manager-3DMYKZPB.js → mind-manager-P5OBDUKI.js} +5 -6
  56. package/dist/mind-sleep-FWRBIFBS.js +41 -0
  57. package/dist/mind-wake-LJK2YU5X.js +36 -0
  58. package/dist/{package-4NHAVUUI.js → package-A7PEYJI2.js} +10 -1
  59. package/dist/{pages-4DGQT7ZA.js → pages-YSTRWJR4.js} +6 -6
  60. package/dist/{publish-TAJUET4I.js → publish-BZNHKUUK.js} +6 -6
  61. package/dist/{pull-XAEWQJ47.js → pull-GRQAXM2E.js} +2 -2
  62. package/dist/{register-VSPCMHKX.js → register-U2UO6TC4.js} +5 -5
  63. package/dist/registry-D2BSQ2X5.js +42 -0
  64. package/dist/{restart-IQKMCK5M.js → restart-CIDAKGG2.js} +3 -6
  65. package/dist/{schedule-FFZG23IW.js → schedule-NLR3LZLY.js} +2 -2
  66. package/dist/{seed-J43YDKXG.js → seed-3H2MRREW.js} +2 -2
  67. package/dist/{send-KVIZIGCE.js → send-RP2TA7SG.js} +132 -36
  68. package/dist/{service-LUR7WDO7.js → service-7BFXDI6J.js} +31 -13
  69. package/dist/{setup-52YRV7VP.js → setup-SSIIXQMI.js} +9 -34
  70. package/dist/{shared-KO35ZM44.js → shared-2OGT3NSL.js} +4 -4
  71. package/dist/{skill-BCVNI6TV.js → skill-Q2Y6PQ3L.js} +2 -2
  72. package/dist/skills/orientation/SKILL.md +2 -2
  73. package/dist/skills/volute-mind/SKILL.md +5 -5
  74. package/dist/sleep-manager-3RWUX2ZR.js +27 -0
  75. package/dist/{sprout-QN7Y4VVO.js → sprout-UKCYBGHK.js} +34 -30
  76. package/dist/{start-I5JYB65M.js → start-JR6CUUWF.js} +3 -6
  77. package/dist/{status-D7E5HHBV.js → status-5XDGYHKP.js} +2 -2
  78. package/dist/{status-4ESFLGH4.js → status-H2MKDN6L.js} +5 -5
  79. package/dist/{status-FU2PFVVF.js → status-LV34BG6G.js} +3 -3
  80. package/dist/{stop-NBVKEFQQ.js → stop-VKPGK25U.js} +2 -5
  81. package/dist/template-hash-BIMA4ILT.js +8 -0
  82. package/dist/{up-FS7CKM6V.js → up-JKGC7PPF.js} +5 -5
  83. package/dist/{update-FJIHDJKM.js → update-ELC6MEUT.js} +5 -5
  84. package/dist/{update-check-MWE5AH4U.js → update-check-F5Z3ALXX.js} +2 -2
  85. package/dist/{upgrade-AIT24B5I.js → upgrade-GXW2EQY3.js} +12 -3
  86. package/dist/{variant-63ZWO2W7.js → variant-A4I7PHXS.js} +16 -24
  87. package/dist/version-notify-5FGUAVSF.js +181 -0
  88. package/dist/web-assets/assets/index-DWBxl4LO.js +69 -0
  89. package/dist/web-assets/assets/index-ZqMd1mx1.css +1 -0
  90. package/dist/web-assets/index.html +2 -2
  91. package/package.json +10 -1
  92. package/templates/_base/.init/.config/prompts.json +1 -0
  93. package/templates/_base/home/.config/config.json.tmpl +4 -1
  94. package/templates/_base/src/lib/logger.ts +68 -23
  95. package/templates/_base/src/lib/startup.ts +12 -3
  96. package/templates/claude/src/agent.ts +150 -29
  97. package/templates/claude/src/lib/hooks/pre-compact.ts +18 -4
  98. package/templates/claude/src/lib/message-channel.ts +6 -0
  99. package/templates/claude/src/lib/stream-consumer.ts +7 -0
  100. package/templates/claude/src/server.ts +3 -1
  101. package/templates/pi/home/.config/config.json.tmpl +4 -1
  102. package/templates/pi/src/agent.ts +87 -0
  103. package/templates/pi/src/lib/event-handler.ts +13 -1
  104. package/templates/pi/src/server.ts +3 -1
  105. package/dist/chunk-5XNT2472.js +0 -36
  106. package/dist/chunk-FGSYHIS3.js +0 -891
  107. package/dist/chunk-UJ6GHNR7.js +0 -675
  108. package/dist/db-C2CJ46ZU.js +0 -10
  109. package/dist/delivery-manager-CSG7LXA4.js +0 -16
  110. package/dist/down-ZY35KMHR.js +0 -14
  111. package/dist/schema-GFH6RV3W.js +0 -26
  112. package/dist/variants-JAGWGBXG.js +0 -26
  113. package/dist/web-assets/assets/index-CUZTZzaW.js +0 -64
  114. package/dist/web-assets/assets/index-adVuCkqy.css +0 -1
@@ -2,6 +2,7 @@ import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
2
2
 
3
3
  export type MessageChannel = {
4
4
  push: (msg: SDKUserMessage) => void;
5
+ drain: () => SDKUserMessage[];
5
6
  iterable: AsyncIterable<SDKUserMessage>;
6
7
  };
7
8
 
@@ -19,6 +20,11 @@ export function createMessageChannel(): MessageChannel {
19
20
  queue.push(msg);
20
21
  }
21
22
  },
23
+ drain() {
24
+ // Clear any pending iterator wait so it doesn't consume a message after drain
25
+ resolve = null;
26
+ return queue.splice(0);
27
+ },
22
28
  iterable: {
23
29
  [Symbol.asyncIterator]() {
24
30
  return {
@@ -15,6 +15,7 @@ export type StreamCallbacks = {
15
15
  onSessionId?: (sessionId: string) => void;
16
16
  broadcast: (event: VoluteEvent) => void;
17
17
  onTurnEnd?: () => void;
18
+ onContextTokens?: (tokens: number) => void;
18
19
  };
19
20
 
20
21
  // Loaded once at startup — mind restarts on config changes
@@ -50,6 +51,12 @@ export async function consumeStream(
50
51
  callbacks.onSessionId?.(msg.session_id as string);
51
52
  }
52
53
  if (msg.type === "assistant") {
54
+ const usage = msg.message.usage as unknown as Record<string, unknown> | undefined;
55
+ const inputTokens = (usage?.input_tokens as number) ?? 0;
56
+ const cacheCreation = (usage?.cache_creation_input_tokens as number) ?? 0;
57
+ const cacheRead = (usage?.cache_read_input_tokens as number) ?? 0;
58
+ const contextTokens = inputTokens + cacheCreation + cacheRead;
59
+ if (contextTokens) callbacks.onContextTokens?.(contextTokens);
53
60
  for (const b of msg.message.content) {
54
61
  if (b.type === "thinking" && "thinking" in b && b.thinking) {
55
62
  const text = b.thinking as string;
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { createMind } from "./agent.js";
4
4
  import { daemonRestart } from "./lib/daemon-client.js";
5
5
  import { createFileHandlerResolver } from "./lib/file-handler.js";
6
- import { log } from "./lib/logger.js";
6
+ import { log, setLevel } from "./lib/logger.js";
7
7
  import { createRouter } from "./lib/router.js";
8
8
  import {
9
9
  loadConfig,
@@ -16,6 +16,7 @@ import { createVoluteServer } from "./lib/volute-server.js";
16
16
 
17
17
  const { port } = parseArgs();
18
18
  const config = loadConfig();
19
+ if (config.logLevel) setLevel(config.logLevel);
19
20
  if (config.model) log("server", `using model: ${config.model}`);
20
21
  if (config.maxThinkingTokens) log("server", `max thinking tokens: ${config.maxThinkingTokens}`);
21
22
 
@@ -40,6 +41,7 @@ const mind = createMind({
40
41
  maxThinkingTokens: config.maxThinkingTokens,
41
42
  sessionsDir,
42
43
  compactionMessage: config.compactionMessage,
44
+ maxContextTokens: config.compaction?.maxContextTokens,
43
45
  onIdentityReload: async () => {
44
46
  log("server", "identity file changed — restarting to reload");
45
47
  await mind.waitForCommits();
@@ -1,3 +1,6 @@
1
1
  {
2
- "model": "openrouter:moonshotai/kimi-k2.5"
2
+ "model": "openrouter:moonshotai/kimi-k2.5",
3
+ "compaction": {
4
+ "maxContextTokens": 150000
5
+ }
3
6
  }
@@ -43,12 +43,19 @@ export function createMind(options: {
43
43
  model?: string;
44
44
  thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
45
45
  compactionMessage?: string;
46
+ maxContextTokens?: number;
46
47
  }): { resolve: HandlerResolver } {
47
48
  const sessions = new Map<string, PiSession>();
48
49
  const prompts = loadPrompts();
49
50
  const today = new Date().toLocaleDateString("en-CA");
50
51
  const compactionMessage =
51
52
  options.compactionMessage ?? prompts.compaction_warning.replace("${date}", today);
53
+ const compactionInstructions = prompts.compaction_instructions;
54
+ const maxContextTokens = options.maxContextTokens;
55
+
56
+ if (maxContextTokens) {
57
+ log("mind", `compaction threshold: ${maxContextTokens} tokens`);
58
+ }
52
59
 
53
60
  // Shared setup (created once)
54
61
  const modelStr = options.model || process.env.PI_MODEL || "anthropic:claude-sonnet-4-20250514";
@@ -88,9 +95,35 @@ export function createMind(options: {
88
95
 
89
96
  log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
90
97
 
98
+ // Compaction state machine:
99
+ // 1. onContextTokens sets compactionTriggered=true and sends warning
100
+ // 2. onTurnEnd (after warning turn): compactionTriggered -> compactOnNextTurnEnd
101
+ // 3. onTurnEnd (after mind's save turn): compactOnNextTurnEnd -> call compact()
91
102
  let compactBlocked = false;
103
+ let manualCompactPending = false;
104
+ let compactionTriggered = false;
105
+ let compactOnNextTurnEnd = false;
106
+ let compactionInProgress = false;
107
+
108
+ function resetCompactionState() {
109
+ compactionTriggered = false;
110
+ compactOnNextTurnEnd = false;
111
+ compactionInProgress = false;
112
+ }
113
+
92
114
  const preCompactExtension: ExtensionFactory = (pi) => {
93
115
  pi.on("session_before_compact", () => {
116
+ // Our programmatic compact() call (triggered by token threshold) — allow through
117
+ if (manualCompactPending) {
118
+ manualCompactPending = false;
119
+ log(
120
+ "mind",
121
+ `session "${session.name}": allowing manual compaction with custom instructions`,
122
+ );
123
+ return;
124
+ }
125
+
126
+ // Auto-compaction: two-pass block (first pass warns mind, second pass allows)
94
127
  if (!compactBlocked) {
95
128
  compactBlocked = true;
96
129
  log(
@@ -146,6 +179,60 @@ export function createMind(options: {
146
179
  createEventHandler(session, {
147
180
  cwd: options.cwd,
148
181
  broadcast: (event) => broadcast(session, event),
182
+ onContextTokens: maxContextTokens
183
+ ? (tokens: number) => {
184
+ if (tokens >= maxContextTokens && !compactionTriggered && !compactionInProgress) {
185
+ if (!session.agentSession) {
186
+ log(
187
+ "mind",
188
+ `session "${session.name}": compaction threshold hit but session not ready`,
189
+ );
190
+ return;
191
+ }
192
+ compactionTriggered = true;
193
+ log(
194
+ "mind",
195
+ `session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — triggering compaction`,
196
+ );
197
+ // Send compaction warning; compaction will follow after the mind finishes its response turn
198
+ session.messageIds.push(undefined);
199
+ session.agentSession.prompt(compactionMessage, {
200
+ streamingBehavior: "followUp",
201
+ });
202
+ }
203
+ }
204
+ : undefined,
205
+ onTurnEnd: maxContextTokens
206
+ ? () => {
207
+ try {
208
+ // Compact on the turn AFTER the warning was sent (so the mind gets a turn to save state)
209
+ if (compactOnNextTurnEnd) {
210
+ compactOnNextTurnEnd = false;
211
+ manualCompactPending = true;
212
+ compactionInProgress = true;
213
+ log("mind", `session "${session.name}": compacting with custom instructions`);
214
+ Promise.resolve(session.agentSession?.compact(compactionInstructions))
215
+ .catch((err) =>
216
+ log("mind", `session "${session.name}": compact() failed:`, err),
217
+ )
218
+ .finally(() => {
219
+ compactionInProgress = false;
220
+ });
221
+ }
222
+ if (compactionTriggered) {
223
+ compactionTriggered = false;
224
+ compactOnNextTurnEnd = true;
225
+ }
226
+ } catch (err) {
227
+ log(
228
+ "mind",
229
+ `session "${session.name}": onTurnEnd error, resetting compaction state:`,
230
+ err,
231
+ );
232
+ resetCompactionState();
233
+ }
234
+ }
235
+ : undefined,
149
236
  }),
150
237
  );
151
238
 
@@ -14,6 +14,8 @@ export type EventSession = {
14
14
  export type EventHandlerOptions = {
15
15
  cwd: string;
16
16
  broadcast: (event: VoluteEvent) => void;
17
+ onContextTokens?: (tokens: number) => void;
18
+ onTurnEnd?: () => void;
17
19
  };
18
20
 
19
21
  // Loaded once at startup — mind restarts on config changes
@@ -133,14 +135,20 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
133
135
  session.messageChannels.delete(session.currentMessageId);
134
136
  }
135
137
  log("mind", `session "${session.name}": turn done`);
136
- // Sum usage from assistant messages
138
+ // Sum usage from assistant messages. The last assistant message's input tokens
139
+ // approximate current context size (it includes the full conversation up to that point).
137
140
  if (event.messages) {
138
141
  let inputTokens = 0;
139
142
  let outputTokens = 0;
143
+ let lastInputTokens = 0;
140
144
  for (const msg of event.messages as any[]) {
141
145
  if (msg.role === "assistant" && msg.usage) {
142
146
  inputTokens += msg.usage.input ?? 0;
143
147
  outputTokens += msg.usage.output ?? 0;
148
+ const cacheWrite = msg.usage.cacheWrite ?? msg.usage.cache_creation ?? 0;
149
+ const cacheRead = msg.usage.cacheRead ?? msg.usage.cache_read ?? 0;
150
+ const contextTokens = (msg.usage.input ?? 0) + cacheWrite + cacheRead;
151
+ if (contextTokens) lastInputTokens = contextTokens;
144
152
  }
145
153
  }
146
154
  if (inputTokens > 0 || outputTokens > 0) {
@@ -148,10 +156,14 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
148
156
  options.broadcast({ type: "usage", ...usage });
149
157
  emit(session, { type: "usage", metadata: usage });
150
158
  }
159
+ if (lastInputTokens > 0) {
160
+ options.onContextTokens?.(lastInputTokens);
161
+ }
151
162
  }
152
163
  options.broadcast({ type: "done" });
153
164
  emit(session, { type: "done" });
154
165
  session.currentMessageId = undefined;
166
+ options.onTurnEnd?.();
155
167
  }
156
168
  } catch (err) {
157
169
  log("mind", `session "${session.name}": event handler error (${event?.type}):`, err);
@@ -1,7 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { createMind } from "./agent.js";
3
3
  import { createFileHandlerResolver } from "./lib/file-handler.js";
4
- import { log } from "./lib/logger.js";
4
+ import { log, setLevel } from "./lib/logger.js";
5
5
  import { createRouter } from "./lib/router.js";
6
6
  import {
7
7
  handleStartupContext,
@@ -15,6 +15,7 @@ import { createVoluteServer } from "./lib/volute-server.js";
15
15
 
16
16
  const { port } = parseArgs();
17
17
  const config = loadConfig();
18
+ if (config.logLevel) setLevel(config.logLevel);
18
19
  if (config.model) log("server", `using model: ${config.model}`);
19
20
  if (config.thinkingLevel) log("server", `thinking level: ${config.thinkingLevel}`);
20
21
 
@@ -29,6 +30,7 @@ const mind = createMind({
29
30
  model: config.model,
30
31
  thinkingLevel: config.thinkingLevel,
31
32
  compactionMessage: config.compactionMessage,
33
+ maxContextTokens: config.compaction?.maxContextTokens,
32
34
  });
33
35
 
34
36
  const router = createRouter({
@@ -1,36 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- schema_exports
4
- } from "./chunk-NSE7VJQA.js";
5
- import {
6
- voluteHome
7
- } from "./chunk-EBGCNDMM.js";
8
-
9
- // src/lib/db.ts
10
- import { chmodSync, existsSync } from "fs";
11
- import { dirname, resolve } from "path";
12
- import { fileURLToPath } from "url";
13
- import { drizzle } from "drizzle-orm/libsql";
14
- import { migrate } from "drizzle-orm/libsql/migrator";
15
- var __dirname = dirname(fileURLToPath(import.meta.url));
16
- var migrationsFolder = existsSync(resolve(__dirname, "../drizzle")) ? resolve(__dirname, "../drizzle") : resolve(__dirname, "../../drizzle");
17
- var db = null;
18
- async function getDb() {
19
- if (db) return db;
20
- const dbPath = process.env.VOLUTE_DB_PATH || resolve(voluteHome(), "volute.db");
21
- db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
22
- await migrate(db, { migrationsFolder });
23
- try {
24
- chmodSync(dbPath, 384);
25
- } catch (err) {
26
- console.error(
27
- `[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
28
- err
29
- );
30
- }
31
- return db;
32
- }
33
-
34
- export {
35
- getDb
36
- };