volute 0.39.0 → 0.40.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 (108) hide show
  1. package/dist/{activity-events-CR4RIHJT.js → activity-events-OJZ7G5CG.js} +3 -3
  2. package/dist/{ai-service-PL7GKFQC.js → ai-service-JMV4KZN4.js} +8 -4
  3. package/dist/{archive-UEWIFRAR.js → archive-BC2MTNPV.js} +3 -3
  4. package/dist/{auth-OJ2YW3NR.js → auth-2IMZ6N5G.js} +4 -4
  5. package/dist/{chat-RQ5NV5EA.js → chat-O5QCGCUG.js} +2 -2
  6. package/dist/{chunk-TCNCRDB5.js → chunk-2AQCEQYN.js} +1 -1
  7. package/dist/{chunk-OEXOFBW5.js → chunk-3XSFMN3H.js} +1 -1
  8. package/dist/{chunk-7OS7QFDF.js → chunk-43PVCF4V.js} +3 -3
  9. package/dist/{chunk-7GZB3ZJP.js → chunk-5VBXKFPO.js} +2 -2
  10. package/dist/{chunk-IA2SZSLB.js → chunk-76Q6YLJL.js} +1 -1
  11. package/dist/{chunk-QDWPM73D.js → chunk-B7YJUWMM.js} +3 -3
  12. package/dist/{chunk-TT4WT6Y7.js → chunk-DNQPHLGM.js} +1 -1
  13. package/dist/{chunk-6FSSMAXU.js → chunk-FBV22PFY.js} +1 -1
  14. package/dist/{chunk-RAVQXKAG.js → chunk-FRJ73IWR.js} +1 -1
  15. package/dist/{chunk-WUH54G5S.js → chunk-HAJHDGBV.js} +1 -1
  16. package/dist/{chunk-X7GM2OPM.js → chunk-HEZM5GCV.js} +2 -2
  17. package/dist/{chunk-4BO4I7UF.js → chunk-JSITYTIL.js} +6 -6
  18. package/dist/{chunk-CJ26DXZL.js → chunk-MHFAWJCC.js} +16 -0
  19. package/dist/{chunk-OS5MRIEP.js → chunk-PC55DGNX.js} +1 -1
  20. package/dist/{chunk-QDWP435B.js → chunk-PDSA3ILI.js} +1 -1
  21. package/dist/{chunk-B7MWXFAH.js → chunk-PGX52NFO.js} +15 -1
  22. package/dist/{chunk-7W3UWCQD.js → chunk-PHR4XQPL.js} +3 -3
  23. package/dist/{chunk-EVKGKQ2P.js → chunk-PIRL2HAO.js} +1 -7
  24. package/dist/{chunk-Y5UC6G3O.js → chunk-PZTVQL3J.js} +2 -2
  25. package/dist/{chunk-DZ4LYU6J.js → chunk-RMZSICLS.js} +1 -1
  26. package/dist/{chunk-XOSAY7PY.js → chunk-S4YO5OWV.js} +1 -1
  27. package/dist/{chunk-6V2U3GFQ.js → chunk-SGC6CDXB.js} +2 -2
  28. package/dist/{chunk-QJ4GU5XT.js → chunk-SOPMVTTW.js} +2 -2
  29. package/dist/{chunk-IBOU6Q3Y.js → chunk-T26YLQPU.js} +5 -5
  30. package/dist/{chunk-Y4W3OS7Q.js → chunk-WGRMLXTH.js} +8 -8
  31. package/dist/{chunk-CHCISTH7.js → chunk-XUJR6NZW.js} +508 -332
  32. package/dist/cli.js +15 -15
  33. package/dist/{cloud-sync-NQCWJLYI.js → cloud-sync-5JF2Z7VM.js} +16 -16
  34. package/dist/{conversations-EUXHBWJD.js → conversations-LUVHGQSS.js} +4 -4
  35. package/dist/{create-3QH7DVLC.js → create-TDQM3SHV.js} +1 -1
  36. package/dist/{daemon-restart-UEB3PLKB.js → daemon-restart-XHDJDOPQ.js} +8 -8
  37. package/dist/daemon.js +219 -116
  38. package/dist/{db-32YFKMFL.js → db-2GIS7HSG.js} +2 -2
  39. package/dist/delivery-manager-L5WFP2ND.js +30 -0
  40. package/dist/{delivery-router-EUW4RIWG.js → delivery-router-IU4OMVKZ.js} +3 -3
  41. package/dist/down-KIJVZ3OC.js +17 -0
  42. package/dist/echo-text-TJWCUBPB.js +31 -0
  43. package/dist/{exec-2MFHTGOS.js → exec-BGMU7VXR.js} +4 -4
  44. package/dist/{export-JDAK3THX.js → export-RKUWSQSG.js} +3 -3
  45. package/dist/{extensions-V7TXKAKM.js → extensions-VN5SEVRP.js} +16 -16
  46. package/dist/{files-K5XV23H6.js → files-J2CKZ6PK.js} +3 -3
  47. package/dist/{import-GZ5IYVV7.js → import-KVY6BYXG.js} +6 -6
  48. package/dist/{isolation-FPF4UTH2.js → isolation-MM7YHK6J.js} +3 -3
  49. package/dist/{login-BGF5JY3I.js → login-OHM32QVD.js} +2 -2
  50. package/dist/{logout-GQHI6KH6.js → logout-74WBDMOE.js} +2 -2
  51. package/dist/{message-delivery-X7MKA3TA.js → message-delivery-VBBAJX3J.js} +16 -16
  52. package/dist/{mind-QMJIWWWW.js → mind-PUPXDIYQ.js} +5 -5
  53. package/dist/{mind-activity-tracker-Z3KBQTVU.js → mind-activity-tracker-7GE24YZE.js} +4 -4
  54. package/dist/mind-manager-4Q6KGZHL.js +30 -0
  55. package/dist/mind-service-7OD4PRO5.js +36 -0
  56. package/dist/{package-IR4ZEQA2.js → package-6YPVWGD3.js} +1 -1
  57. package/dist/{registry-LWEOAO53.js → registry-L272BZJR.js} +2 -2
  58. package/dist/{sandbox-KUAVC37A.js → sandbox-BQAFYHLE.js} +4 -4
  59. package/dist/scheduler-7E22Q7IC.js +30 -0
  60. package/dist/{schema-K575EBPE.js → schema-HHYJDG7B.js} +3 -1
  61. package/dist/{seed-GPBERPXJ.js → seed-4V2O7FQE.js} +1 -1
  62. package/dist/{seed-cmd-PWF4FYFG.js → seed-cmd-JKDEW4MP.js} +2 -2
  63. package/dist/{seed-create-NTIICZVB.js → seed-create-EPAO3H5F.js} +3 -3
  64. package/dist/{seed-sprout-PRPFKGJF.js → seed-sprout-EONCL4DU.js} +8 -8
  65. package/dist/{send-IMHDYJHW.js → send-232RAHPX.js} +3 -3
  66. package/dist/{service-DWBRRFXS.js → service-AG5BKRUV.js} +5 -5
  67. package/dist/{service-install-KEBDNZVE.js → service-install-HVAEUBAH.js} +6 -6
  68. package/dist/{setup-IFYS3TLS.js → setup-IL7QJZN4.js} +3 -3
  69. package/dist/{setup-4CWQ53VV.js → setup-PSA7LJ5U.js} +8 -8
  70. package/dist/{skills-FJ5W73AV.js → skills-SLEY5X6W.js} +6 -6
  71. package/dist/sleep-manager-27S7MKJO.js +34 -0
  72. package/dist/{spirit-U6MDQLYT.js → spirit-7BUHSEO2.js} +8 -8
  73. package/dist/{sprout-ABX2G76C.js → sprout-JWQ57KF3.js} +1 -1
  74. package/dist/{status-C6BBWNU3.js → status-6GSOSYOD.js} +6 -6
  75. package/dist/system-chat-EIINIHFB.js +34 -0
  76. package/dist/{tailscale-2KKXDMNK.js → tailscale-6MHNLBAX.js} +2 -2
  77. package/dist/up-4XS3RCPP.js +19 -0
  78. package/dist/{update-V26PT4VN.js → update-BT2PY73H.js} +6 -6
  79. package/dist/{update-check-XRERV36Y.js → update-check-Y3HBEMF2.js} +3 -3
  80. package/dist/{version-notify-ZBMY2TEX.js → version-notify-NFQMI5IY.js} +17 -17
  81. package/dist/web-assets/assets/index-DqHSW9Fr.css +1 -0
  82. package/dist/web-assets/assets/index-Jluza4c-.js +73 -0
  83. package/dist/web-assets/index.html +2 -2
  84. package/drizzle/0009_mind_notices.sql +14 -0
  85. package/drizzle/meta/_journal.json +7 -0
  86. package/package.json +1 -1
  87. package/templates/_base/.init/.local/hooks/pre-prompt/notices.ts +46 -0
  88. package/templates/_base/src/lib/context-breakdown.ts +281 -126
  89. package/templates/_base/src/lib/daemon-client.ts +5 -2
  90. package/templates/_base/src/lib/transparency.ts +5 -3
  91. package/templates/_base/src/lib/volute-server.ts +23 -0
  92. package/templates/claude/src/agent.ts +54 -16
  93. package/templates/claude/src/server.ts +1 -0
  94. package/templates/codex/src/agent.ts +45 -12
  95. package/templates/codex/src/server.ts +1 -0
  96. package/templates/pi/src/agent.ts +72 -13
  97. package/templates/pi/src/server.ts +1 -0
  98. package/dist/delivery-manager-N34XKUGL.js +0 -30
  99. package/dist/down-TCEBGTMY.js +0 -17
  100. package/dist/echo-text-N3BC3WNX.js +0 -31
  101. package/dist/mind-manager-7GWTTKWD.js +0 -30
  102. package/dist/mind-service-MO2XJKJN.js +0 -36
  103. package/dist/scheduler-CGUPAEBA.js +0 -30
  104. package/dist/sleep-manager-6QRMUNHK.js +0 -34
  105. package/dist/system-chat-D343HYGR.js +0 -34
  106. package/dist/up-ZU6ILVMA.js +0 -19
  107. package/dist/web-assets/assets/index-B1zwqwt8.css +0 -1
  108. package/dist/web-assets/assets/index-D1guJF1i.js +0 -73
@@ -29,6 +29,26 @@ function countTokens(text: string): number {
29
29
  return _countTokens(text);
30
30
  }
31
31
 
32
+ // --- Shared JSONL reader ---
33
+
34
+ function readJsonlEntries<T>(filePath: string): T[] | null {
35
+ let data: string;
36
+ try {
37
+ data = readFileSync(filePath, "utf-8");
38
+ } catch (err: any) {
39
+ if (err?.code !== "ENOENT") console.warn(`context-breakdown: ${filePath}:`, err?.message);
40
+ return null;
41
+ }
42
+
43
+ const entries: T[] = [];
44
+ for (const line of data.split("\n").filter((l) => l.trim())) {
45
+ try {
46
+ entries.push(JSON.parse(line));
47
+ } catch {}
48
+ }
49
+ return entries;
50
+ }
51
+
32
52
  // --- Claude JSONL parser ---
33
53
 
34
54
  type ClaudeContentBlock = {
@@ -37,6 +57,8 @@ type ClaudeContentBlock = {
37
57
  thinking?: string;
38
58
  input?: unknown;
39
59
  content?: unknown;
60
+ name?: string;
61
+ is_error?: boolean;
40
62
  };
41
63
 
42
64
  type ClaudeMessage = {
@@ -57,32 +79,52 @@ export type ParsedContext = {
57
79
  breakdown: ContextBreakdown;
58
80
  };
59
81
 
60
- export function parseClaudeSessionJSONL(
82
+ // --- Message extraction types ---
83
+
84
+ export type ContextBlock =
85
+ | { type: "text"; text: string }
86
+ | { type: "thinking"; text: string }
87
+ | { type: "tool_use"; name: string; input: string }
88
+ | { type: "tool_result"; text: string; isError?: boolean };
89
+
90
+ export type ContextMessage = {
91
+ role: "user" | "assistant" | "system";
92
+ blocks: ContextBlock[];
93
+ };
94
+
95
+ export type ContextMessages = {
96
+ preamble: {
97
+ systemPrompt: string;
98
+ sdkInstructions: string;
99
+ skillDescriptions: Array<{ name: string; description: string }>;
100
+ };
101
+ sessions: Array<{
102
+ name: string;
103
+ messages: ContextMessage[];
104
+ }>;
105
+ };
106
+
107
+ // --- Claude: combined parse + extract ---
108
+
109
+ type SessionResult = {
110
+ parsed: ParsedContext | null;
111
+ messages: ContextMessage[];
112
+ };
113
+
114
+ export function processClaudeSession(
61
115
  filePath: string,
62
116
  systemPromptTokens: number,
63
117
  claudeMdTokens: number,
64
118
  skillDescriptionTokens: number,
65
- ): ParsedContext | null {
66
- let data: string;
67
- try {
68
- data = readFileSync(filePath, "utf-8");
69
- } catch (err: any) {
70
- if (err?.code !== "ENOENT") console.warn(`context-breakdown: ${filePath}:`, err?.message);
71
- return null;
72
- }
119
+ ): SessionResult {
120
+ const entries = readJsonlEntries<ClaudeMessage>(filePath);
121
+ if (!entries) return { parsed: null, messages: [] };
73
122
 
74
- const lines = data.split("\n").filter((l) => l.trim());
75
123
  let lastContextTokens = 0;
76
124
  const conv = { userText: 0, assistantText: 0, thinking: 0, toolUse: 0, toolResult: 0 };
125
+ const messages: ContextMessage[] = [];
77
126
 
78
- for (const line of lines) {
79
- let entry: ClaudeMessage;
80
- try {
81
- entry = JSON.parse(line);
82
- } catch {
83
- continue;
84
- }
85
-
127
+ for (const entry of entries) {
86
128
  if (entry.type === "assistant" && entry.message) {
87
129
  const usage = entry.message.usage;
88
130
  if (usage) {
@@ -92,49 +134,80 @@ export function parseClaudeSessionJSONL(
92
134
  (usage.cache_read_input_tokens ?? 0);
93
135
  if (ctx > 0) lastContextTokens = ctx;
94
136
  }
137
+ const blocks: ContextBlock[] = [];
95
138
  for (const block of entry.message.content ?? []) {
96
139
  if (block.type === "thinking" && block.thinking) {
97
140
  conv.thinking += countTokens(block.thinking);
141
+ blocks.push({ type: "thinking", text: block.thinking });
98
142
  } else if (block.type === "text" && block.text) {
99
143
  conv.assistantText += countTokens(block.text);
144
+ blocks.push({ type: "text", text: block.text });
100
145
  } else if (block.type === "tool_use") {
101
146
  const input = block.input;
147
+ const inputStr = typeof input === "string" ? input : JSON.stringify(input ?? {}, null, 2);
102
148
  conv.toolUse += countTokens(
103
149
  typeof input === "string" ? input : JSON.stringify(input ?? {}),
104
150
  );
151
+ blocks.push({ type: "tool_use", name: block.name ?? "unknown", input: inputStr });
105
152
  }
106
153
  }
154
+ if (blocks.length > 0) messages.push({ role: "assistant", blocks });
107
155
  } else if (entry.type === "user" && entry.message) {
156
+ const blocks: ContextBlock[] = [];
108
157
  for (const block of entry.message.content ?? []) {
109
158
  if (block.type === "tool_result") {
110
159
  const content = block.content;
160
+ const isError = block.is_error === true;
111
161
  if (typeof content === "string") {
112
162
  conv.toolResult += countTokens(content);
163
+ blocks.push({ type: "tool_result", text: content, isError });
113
164
  } else if (Array.isArray(content)) {
114
165
  for (const c of content) {
115
166
  if (c && typeof c === "object" && "text" in c && typeof c.text === "string") {
116
167
  conv.toolResult += countTokens(c.text);
168
+ blocks.push({ type: "tool_result", text: c.text, isError });
117
169
  }
118
170
  }
119
171
  }
120
172
  } else if (block.type === "text" && block.text) {
121
173
  conv.userText += countTokens(block.text);
174
+ blocks.push({ type: "text", text: block.text });
122
175
  }
123
176
  }
177
+ if (blocks.length > 0) messages.push({ role: "user", blocks });
124
178
  }
125
179
  }
126
180
 
127
- if (lastContextTokens === 0) return null;
181
+ const parsed =
182
+ lastContextTokens === 0
183
+ ? null
184
+ : {
185
+ contextTokens: lastContextTokens,
186
+ breakdown: {
187
+ systemPrompt: systemPromptTokens,
188
+ sdkInstructions: claudeMdTokens,
189
+ skillDescriptions: skillDescriptionTokens,
190
+ conversation: conv,
191
+ },
192
+ };
193
+
194
+ return { parsed, messages };
195
+ }
128
196
 
129
- return {
130
- contextTokens: lastContextTokens,
131
- breakdown: {
132
- systemPrompt: systemPromptTokens,
133
- sdkInstructions: claudeMdTokens,
134
- skillDescriptions: skillDescriptionTokens,
135
- conversation: conv,
136
- },
137
- };
197
+ /** @deprecated Use processClaudeSession instead */
198
+ export function parseClaudeSessionJSONL(
199
+ filePath: string,
200
+ systemPromptTokens: number,
201
+ claudeMdTokens: number,
202
+ skillDescriptionTokens: number,
203
+ ): ParsedContext | null {
204
+ return processClaudeSession(filePath, systemPromptTokens, claudeMdTokens, skillDescriptionTokens)
205
+ .parsed;
206
+ }
207
+
208
+ /** @deprecated Use processClaudeSession instead */
209
+ export function extractClaudeSessionMessages(filePath: string): ContextMessage[] {
210
+ return processClaudeSession(filePath, 0, 0, 0).messages;
138
211
  }
139
212
 
140
213
  // --- Codex JSONL parser ---
@@ -160,32 +233,22 @@ type CodexEntry = {
160
233
  };
161
234
  };
162
235
 
163
- export function parseCodexSessionJSONL(
236
+ // --- Codex: combined parse + extract ---
237
+
238
+ export function processCodexSession(
164
239
  filePath: string,
165
240
  systemPromptTokens: number,
166
241
  claudeMdTokens: number,
167
242
  skillDescriptionTokens: number,
168
- ): ParsedContext | null {
169
- let data: string;
170
- try {
171
- data = readFileSync(filePath, "utf-8");
172
- } catch (err: any) {
173
- if (err?.code !== "ENOENT") console.warn(`context-breakdown: ${filePath}:`, err?.message);
174
- return null;
175
- }
243
+ ): SessionResult {
244
+ const entries = readJsonlEntries<CodexEntry>(filePath);
245
+ if (!entries) return { parsed: null, messages: [] };
176
246
 
177
- const lines = data.split("\n").filter((l) => l.trim());
178
247
  let lastContextTokens = 0;
179
248
  const conv = { userText: 0, assistantText: 0, thinking: 0, toolUse: 0, toolResult: 0 };
249
+ const messages: ContextMessage[] = [];
180
250
 
181
- for (const line of lines) {
182
- let entry: CodexEntry;
183
- try {
184
- entry = JSON.parse(line);
185
- } catch {
186
- continue;
187
- }
188
-
251
+ for (const entry of entries) {
189
252
  const payload = entry.payload;
190
253
  if (!payload) continue;
191
254
 
@@ -196,45 +259,83 @@ export function parseCodexSessionJSONL(
196
259
  }
197
260
  } else if (entry.type === "response_item") {
198
261
  if (payload.type === "reasoning") {
199
- // Reasoning summaries are in the `summary` field (array of {text} objects)
200
262
  const summary = payload.summary;
263
+ const blocks: ContextBlock[] = [];
201
264
  if (Array.isArray(summary)) {
202
265
  for (const s of summary) {
203
- if (s && typeof s === "object" && s.text) conv.thinking += countTokens(s.text);
266
+ if (s && typeof s === "object" && s.text) {
267
+ conv.thinking += countTokens(s.text);
268
+ blocks.push({ type: "thinking", text: s.text });
269
+ }
204
270
  }
205
271
  } else if (typeof summary === "string" && summary) {
206
272
  conv.thinking += countTokens(summary);
273
+ blocks.push({ type: "thinking", text: summary });
207
274
  }
275
+ if (blocks.length > 0) messages.push({ role: "assistant", blocks });
208
276
  } else if (payload.type === "message") {
209
277
  const text = payload.content?.map((c) => c.text ?? "").join("") ?? "";
210
278
  if (text) {
211
- if (payload.role === "assistant") {
279
+ const role: ContextMessage["role"] =
280
+ payload.role === "assistant"
281
+ ? "assistant"
282
+ : payload.role === "developer"
283
+ ? "system"
284
+ : "user";
285
+ if (role === "assistant") {
212
286
  conv.assistantText += countTokens(text);
213
287
  } else {
214
288
  conv.userText += countTokens(text);
215
289
  }
290
+ messages.push({ role, blocks: [{ type: "text", text }] });
216
291
  }
217
292
  } else if (payload.type === "function_call") {
218
293
  const args = payload.arguments ?? "";
219
294
  if (args) conv.toolUse += countTokens(args);
295
+ messages.push({
296
+ role: "assistant",
297
+ blocks: [{ type: "tool_use", name: payload.name ?? "unknown", input: args }],
298
+ });
220
299
  } else if (payload.type === "function_call_output") {
221
300
  const output = payload.output ?? "";
222
- if (output) conv.toolResult += countTokens(output);
301
+ if (output) {
302
+ conv.toolResult += countTokens(output);
303
+ messages.push({ role: "user", blocks: [{ type: "tool_result", text: output }] });
304
+ }
223
305
  }
224
306
  }
225
307
  }
226
308
 
227
- if (lastContextTokens === 0) return null;
309
+ const parsed =
310
+ lastContextTokens === 0
311
+ ? null
312
+ : {
313
+ contextTokens: lastContextTokens,
314
+ breakdown: {
315
+ systemPrompt: systemPromptTokens,
316
+ sdkInstructions: claudeMdTokens,
317
+ skillDescriptions: skillDescriptionTokens,
318
+ conversation: conv,
319
+ },
320
+ };
321
+
322
+ return { parsed, messages };
323
+ }
228
324
 
229
- return {
230
- contextTokens: lastContextTokens,
231
- breakdown: {
232
- systemPrompt: systemPromptTokens,
233
- sdkInstructions: claudeMdTokens,
234
- skillDescriptions: skillDescriptionTokens,
235
- conversation: conv,
236
- },
237
- };
325
+ /** @deprecated Use processCodexSession instead */
326
+ export function parseCodexSessionJSONL(
327
+ filePath: string,
328
+ systemPromptTokens: number,
329
+ claudeMdTokens: number,
330
+ skillDescriptionTokens: number,
331
+ ): ParsedContext | null {
332
+ return processCodexSession(filePath, systemPromptTokens, claudeMdTokens, skillDescriptionTokens)
333
+ .parsed;
334
+ }
335
+
336
+ /** @deprecated Use processCodexSession instead */
337
+ export function extractCodexSessionMessages(filePath: string): ContextMessage[] {
338
+ return processCodexSession(filePath, 0, 0, 0).messages;
238
339
  }
239
340
 
240
341
  // --- Pi JSONL parser ---
@@ -245,41 +346,38 @@ type PiEntry = {
245
346
  message?: {
246
347
  role?: string;
247
348
  usage?: { input?: number; cacheRead?: number; cacheWrite?: number };
248
- content?: Array<{ type?: string; text?: string; thinking?: string; arguments?: unknown }>;
349
+ content?: Array<{
350
+ type?: string;
351
+ text?: string;
352
+ thinking?: string;
353
+ arguments?: unknown;
354
+ name?: string;
355
+ }>;
249
356
  };
250
357
  // custom_message entries (hook-injected context: reply-instructions, startup-context, etc.)
251
358
  content?: string;
252
359
  };
253
360
 
254
- export function parsePiSessionJSONL(
361
+ // --- Pi: combined parse + extract ---
362
+
363
+ export function processPiSession(
255
364
  filePath: string,
256
365
  systemPromptTokens: number,
257
366
  claudeMdTokens: number,
258
367
  skillDescriptionTokens: number,
259
- ): ParsedContext | null {
260
- let data: string;
261
- try {
262
- data = readFileSync(filePath, "utf-8");
263
- } catch (err: any) {
264
- if (err?.code !== "ENOENT") console.warn(`context-breakdown: ${filePath}:`, err?.message);
265
- return null;
266
- }
368
+ ): SessionResult {
369
+ const entries = readJsonlEntries<PiEntry>(filePath);
370
+ if (!entries) return { parsed: null, messages: [] };
267
371
 
268
- const lines = data.split("\n").filter((l) => l.trim());
269
372
  let lastContextTokens = 0;
270
373
  const conv = { userText: 0, assistantText: 0, thinking: 0, toolUse: 0, toolResult: 0 };
374
+ const messages: ContextMessage[] = [];
271
375
 
272
- for (const line of lines) {
273
- let entry: PiEntry;
274
- try {
275
- entry = JSON.parse(line);
276
- } catch {
277
- continue;
278
- }
279
-
376
+ for (const entry of entries) {
280
377
  // custom_message entries are hook-injected context (reply-instructions, startup-context)
281
378
  if (entry.type === "custom_message" && entry.content) {
282
379
  conv.userText += countTokens(entry.content);
380
+ messages.push({ role: "user", blocks: [{ type: "text", text: entry.content }] });
283
381
  continue;
284
382
  }
285
383
 
@@ -292,75 +390,103 @@ export function parsePiSessionJSONL(
292
390
  const ctx = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
293
391
  if (ctx > 0) lastContextTokens = ctx;
294
392
  }
393
+ const blocks: ContextBlock[] = [];
295
394
  for (const block of msg.content ?? []) {
296
395
  if (block.type === "thinking" && block.thinking) {
297
396
  conv.thinking += countTokens(block.thinking);
397
+ blocks.push({ type: "thinking", text: block.thinking });
298
398
  } else if (block.type === "text" && block.text) {
299
399
  conv.assistantText += countTokens(block.text);
400
+ blocks.push({ type: "text", text: block.text });
300
401
  } else if (block.type === "toolCall") {
301
402
  const args = block.arguments;
403
+ const argsStr = typeof args === "string" ? args : JSON.stringify(args ?? {}, null, 2);
302
404
  conv.toolUse += countTokens(typeof args === "string" ? args : JSON.stringify(args ?? {}));
405
+ blocks.push({ type: "tool_use", name: block.name ?? "unknown", input: argsStr });
303
406
  }
304
407
  }
408
+ if (blocks.length > 0) messages.push({ role: "assistant", blocks });
305
409
  } else if (msg.role === "toolResult") {
410
+ const blocks: ContextBlock[] = [];
306
411
  for (const block of msg.content ?? []) {
307
- if (block.text) conv.toolResult += countTokens(block.text);
412
+ if (block.text) {
413
+ conv.toolResult += countTokens(block.text);
414
+ blocks.push({ type: "tool_result", text: block.text });
415
+ }
308
416
  }
417
+ if (blocks.length > 0) messages.push({ role: "user", blocks });
309
418
  } else if (msg.role === "user") {
419
+ const blocks: ContextBlock[] = [];
310
420
  for (const block of msg.content ?? []) {
311
421
  if (block.type === "text" && block.text) {
312
422
  conv.userText += countTokens(block.text);
423
+ blocks.push({ type: "text", text: block.text });
313
424
  }
314
425
  }
426
+ if (blocks.length > 0) messages.push({ role: "user", blocks });
315
427
  }
316
428
  }
317
429
 
318
- if (lastContextTokens === 0) return null;
319
-
320
- return {
321
- contextTokens: lastContextTokens,
322
- breakdown: {
323
- systemPrompt: systemPromptTokens,
324
- sdkInstructions: claudeMdTokens,
325
- skillDescriptions: skillDescriptionTokens,
326
- conversation: conv,
327
- },
328
- };
430
+ const parsed =
431
+ lastContextTokens === 0
432
+ ? null
433
+ : {
434
+ contextTokens: lastContextTokens,
435
+ breakdown: {
436
+ systemPrompt: systemPromptTokens,
437
+ sdkInstructions: claudeMdTokens,
438
+ skillDescriptions: skillDescriptionTokens,
439
+ conversation: conv,
440
+ },
441
+ };
442
+
443
+ return { parsed, messages };
329
444
  }
330
445
 
331
- // --- Measure known SDK overhead ---
446
+ /** @deprecated Use processPiSession instead */
447
+ export function parsePiSessionJSONL(
448
+ filePath: string,
449
+ systemPromptTokens: number,
450
+ claudeMdTokens: number,
451
+ skillDescriptionTokens: number,
452
+ ): ParsedContext | null {
453
+ return processPiSession(filePath, systemPromptTokens, claudeMdTokens, skillDescriptionTokens)
454
+ .parsed;
455
+ }
332
456
 
333
- /** Count tokens in the actual composed system prompt string. */
334
- export function countSystemPromptTokens(systemPrompt: string): number {
335
- return countTokens(systemPrompt);
457
+ /** @deprecated Use processPiSession instead */
458
+ export function extractPiSessionMessages(filePath: string): ContextMessage[] {
459
+ return processPiSession(filePath, 0, 0, 0).messages;
336
460
  }
337
461
 
338
- /** Count tokens in the SDK instruction file (CLAUDE.md, MINDS.md, or AGENTS.md). */
339
- export function countSdkInstructionTokens(cwd: string): number {
462
+ // --- Preamble text readers ---
463
+
464
+ /** Read the SDK instruction file content (CLAUDE.md, MINDS.md, or AGENTS.md). */
465
+ export function readSdkInstructions(cwd: string): string {
340
466
  for (const name of ["CLAUDE.md", "MINDS.md", "AGENTS.md"]) {
341
467
  try {
342
- const content = readFileSync(resolve(cwd, name), "utf-8");
343
- return countTokens(content);
468
+ return readFileSync(resolve(cwd, name), "utf-8");
344
469
  } catch (err: any) {
345
470
  if (err?.code !== "ENOENT")
346
471
  console.warn(`context-breakdown: ${resolve(cwd, name)}:`, err?.message);
347
472
  }
348
473
  }
349
- return 0;
474
+ return "";
350
475
  }
351
476
 
352
- /** Count tokens in skill description frontmatter (always in context). */
353
- export function countSkillDescriptionTokens(skillsDirs: string[]): number {
354
- let total = 0;
477
+ /** Read skill names and descriptions from SKILL.md frontmatter. */
478
+ export function readSkillDescriptions(
479
+ skillsDirs: string[],
480
+ ): Array<{ name: string; description: string }> {
481
+ const results: Array<{ name: string; description: string }> = [];
355
482
  for (const dir of skillsDirs) {
356
483
  try {
357
484
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
358
485
  if (!entry.isDirectory()) continue;
359
486
  try {
360
487
  const content = readFileSync(resolve(dir, entry.name, "SKILL.md"), "utf-8");
361
- // Extract just the frontmatter description — that's what's always in context
362
488
  const match = content.match(/^description:\s*(.+?)$/m);
363
- if (match) total += countTokens(match[1]);
489
+ if (match) results.push({ name: entry.name, description: match[1] });
364
490
  } catch (err: any) {
365
491
  if (err?.code !== "ENOENT")
366
492
  console.warn(`context-breakdown: SKILL.md in ${entry.name}:`, err?.message);
@@ -371,6 +497,29 @@ export function countSkillDescriptionTokens(skillsDirs: string[]): number {
371
497
  console.warn(`context-breakdown: skills dir ${dir}:`, err?.message);
372
498
  }
373
499
  }
500
+ return results;
501
+ }
502
+
503
+ // --- Measure known SDK overhead ---
504
+
505
+ /** Count tokens in the actual composed system prompt string. */
506
+ export function countSystemPromptTokens(systemPrompt: string): number {
507
+ return countTokens(systemPrompt);
508
+ }
509
+
510
+ /** Count tokens in the SDK instruction file (CLAUDE.md, MINDS.md, or AGENTS.md). */
511
+ export function countSdkInstructionTokens(cwd: string): number {
512
+ const content = readSdkInstructions(cwd);
513
+ return content ? countTokens(content) : 0;
514
+ }
515
+
516
+ /** Count tokens in skill description frontmatter (always in context). */
517
+ export function countSkillDescriptionTokens(skillsDirs: string[]): number {
518
+ const skills = readSkillDescriptions(skillsDirs);
519
+ let total = 0;
520
+ for (const skill of skills) {
521
+ total += countTokens(skill.description);
522
+ }
374
523
  return total;
375
524
  }
376
525
 
@@ -401,40 +550,46 @@ export function findClaudeSessionFile(cwd: string, sessionId: string): string |
401
550
  }
402
551
 
403
552
  /** Find the Codex JSONL file for a thread ID. */
404
- export function findCodexSessionFile(threadId: string): string | null {
405
- const codexDir = resolve(process.env.HOME ?? "", ".codex/sessions");
406
- try {
407
- for (const year of readdirSync(codexDir)) {
408
- const yearDir = resolve(codexDir, year);
409
- try {
410
- if (!statSync(yearDir).isDirectory()) continue;
411
- } catch {
412
- continue;
413
- }
414
- for (const month of readdirSync(yearDir)) {
415
- const monthDir = resolve(yearDir, month);
553
+ export function findCodexSessionFile(threadId: string, mindDir?: string): string | null {
554
+ // Check mind-local Codex sessions first, then global ~/.codex/sessions
555
+ const searchDirs: string[] = [];
556
+ if (mindDir) searchDirs.push(resolve(mindDir, ".mind/codex/sessions"));
557
+ searchDirs.push(resolve(process.env.HOME ?? "", ".codex/sessions"));
558
+
559
+ for (const codexDir of searchDirs) {
560
+ try {
561
+ for (const year of readdirSync(codexDir)) {
562
+ const yearDir = resolve(codexDir, year);
416
563
  try {
417
- if (!statSync(monthDir).isDirectory()) continue;
564
+ if (!statSync(yearDir).isDirectory()) continue;
418
565
  } catch {
419
566
  continue;
420
567
  }
421
- for (const day of readdirSync(monthDir)) {
422
- const dayDir = resolve(monthDir, day);
568
+ for (const month of readdirSync(yearDir)) {
569
+ const monthDir = resolve(yearDir, month);
423
570
  try {
424
- if (!statSync(dayDir).isDirectory()) continue;
571
+ if (!statSync(monthDir).isDirectory()) continue;
425
572
  } catch {
426
573
  continue;
427
574
  }
428
- for (const file of readdirSync(dayDir)) {
429
- if (file.endsWith(".jsonl") && file.includes(threadId)) {
430
- return resolve(dayDir, file);
575
+ for (const day of readdirSync(monthDir)) {
576
+ const dayDir = resolve(monthDir, day);
577
+ try {
578
+ if (!statSync(dayDir).isDirectory()) continue;
579
+ } catch {
580
+ continue;
581
+ }
582
+ for (const file of readdirSync(dayDir)) {
583
+ if (file.endsWith(".jsonl") && file.includes(threadId)) {
584
+ return resolve(dayDir, file);
585
+ }
431
586
  }
432
587
  }
433
588
  }
434
589
  }
590
+ } catch (err: any) {
591
+ if (err?.code !== "ENOENT") console.warn("context-breakdown: codex sessions:", err?.message);
435
592
  }
436
- } catch (err: any) {
437
- if (err?.code !== "ENOENT") console.warn("context-breakdown: codex sessions:", err?.message);
438
593
  }
439
594
  return null;
440
595
  }
@@ -57,6 +57,7 @@ export type EventType =
57
57
  | "usage"
58
58
  | "session_start"
59
59
  | "done"
60
+ | "error"
60
61
  | "inbound"
61
62
  | "outbound"
62
63
  | "context";
@@ -79,8 +80,10 @@ export async function daemonEmit(event: DaemonEvent): Promise<void> {
79
80
  }
80
81
  const url = `http://127.0.0.1:${port}/api/minds/${encodeURIComponent(mind)}/events`;
81
82
  const body = JSON.stringify(event);
82
- // Critical events (done) get retries if lost, turns stay stuck until daemon restart
83
- const maxAttempts = event.type === "done" ? 3 : 1;
83
+ // Critical events get retries: `done` (else turns stay stuck) and `error` (else the
84
+ // mind never learns a failure happened). The daemon is local, so retrying against it
85
+ // is worthwhile even when the model provider is the thing that's failing.
86
+ const maxAttempts = event.type === "done" || event.type === "error" ? 3 : 1;
84
87
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
85
88
  try {
86
89
  const res = await fetch(url, { method: "POST", headers: headers(), body });
@@ -4,7 +4,9 @@ import type { DaemonEvent, EventType } from "./daemon-client.js";
4
4
 
5
5
  export type TransparencyPreset = "transparent" | "standard" | "private" | "silent";
6
6
 
7
- type FilterableEventType = Exclude<EventType, "inbound" | "outbound" | "context">;
7
+ // `error` is an internal daemon signal (emitted directly, never channel-facing), so it
8
+ // isn't subject to transparency filtering — exclude it alongside the communication records.
9
+ type FilterableEventType = Exclude<EventType, "inbound" | "outbound" | "context" | "error">;
8
10
 
9
11
  const PRESET_RULES: Record<
10
12
  TransparencyPreset,
@@ -52,8 +54,8 @@ const PRESET_RULES: Record<
52
54
  },
53
55
  };
54
56
 
55
- // Communication records are always emitted (bypass transparency filtering)
56
- const ALWAYS_ALLOWED: ReadonlySet<string> = new Set(["inbound", "outbound", "context"]);
57
+ // Communication records and the internal error signal bypass transparency filtering
58
+ const ALWAYS_ALLOWED: ReadonlySet<string> = new Set(["inbound", "outbound", "context", "error"]);
57
59
 
58
60
  export function loadTransparencyPreset(): TransparencyPreset {
59
61
  for (const file of ["home/.config/config.json", "home/.config/volute.json"]) {