@caupulican/pi-agent-core 0.80.103 → 0.81.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 (148) hide show
  1. package/dist/compaction/branch-summarization.d.ts +88 -0
  2. package/dist/compaction/branch-summarization.d.ts.map +1 -0
  3. package/dist/{harness/compaction → compaction}/branch-summarization.js +86 -18
  4. package/dist/compaction/branch-summarization.js.map +1 -0
  5. package/dist/compaction/compaction.d.ts +143 -0
  6. package/dist/compaction/compaction.d.ts.map +1 -0
  7. package/dist/{harness/compaction → compaction}/compaction.js +208 -77
  8. package/dist/compaction/compaction.js.map +1 -0
  9. package/dist/compaction/index.d.ts +7 -0
  10. package/dist/compaction/index.d.ts.map +1 -0
  11. package/dist/compaction/index.js +7 -0
  12. package/dist/compaction/index.js.map +1 -0
  13. package/dist/compaction/utils.d.ts +38 -0
  14. package/dist/compaction/utils.d.ts.map +1 -0
  15. package/dist/{harness/compaction → compaction}/utils.js +36 -14
  16. package/dist/compaction/utils.js.map +1 -0
  17. package/dist/index.d.ts +5 -15
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +9 -16
  20. package/dist/index.js.map +1 -1
  21. package/dist/{harness/messages.d.ts → messages.d.ts} +28 -2
  22. package/dist/messages.d.ts.map +1 -0
  23. package/dist/{harness/messages.js → messages.js} +22 -1
  24. package/dist/messages.js.map +1 -0
  25. package/dist/node.d.ts +5 -1
  26. package/dist/node.d.ts.map +1 -1
  27. package/dist/node.js +9 -1
  28. package/dist/node.js.map +1 -1
  29. package/dist/reliability/classifier.d.ts +33 -0
  30. package/dist/reliability/classifier.d.ts.map +1 -0
  31. package/dist/reliability/classifier.js +69 -0
  32. package/dist/reliability/classifier.js.map +1 -0
  33. package/dist/reliability/index.d.ts +5 -0
  34. package/dist/reliability/index.d.ts.map +1 -0
  35. package/dist/reliability/index.js +5 -0
  36. package/dist/reliability/index.js.map +1 -0
  37. package/dist/reliability/node.d.ts +2 -0
  38. package/dist/reliability/node.d.ts.map +1 -0
  39. package/dist/reliability/node.js +2 -0
  40. package/dist/reliability/node.js.map +1 -0
  41. package/dist/reliability/process-tree.d.ts +10 -0
  42. package/dist/reliability/process-tree.d.ts.map +1 -0
  43. package/dist/reliability/process-tree.js +75 -0
  44. package/dist/reliability/process-tree.js.map +1 -0
  45. package/dist/reliability/retry-controller.d.ts +66 -0
  46. package/dist/reliability/retry-controller.d.ts.map +1 -0
  47. package/dist/reliability/retry-controller.js +104 -0
  48. package/dist/reliability/retry-controller.js.map +1 -0
  49. package/dist/reliability/retry.d.ts +19 -0
  50. package/dist/reliability/retry.d.ts.map +1 -0
  51. package/dist/reliability/retry.js +35 -0
  52. package/dist/reliability/retry.js.map +1 -0
  53. package/dist/reliability/watchdogs.d.ts +47 -0
  54. package/dist/reliability/watchdogs.d.ts.map +1 -0
  55. package/dist/reliability/watchdogs.js +159 -0
  56. package/dist/reliability/watchdogs.js.map +1 -0
  57. package/dist/session/message-retention.d.ts +26 -0
  58. package/dist/session/message-retention.d.ts.map +1 -0
  59. package/dist/session/message-retention.js +95 -0
  60. package/dist/session/message-retention.js.map +1 -0
  61. package/dist/session/session-manager.d.ts +350 -0
  62. package/dist/session/session-manager.d.ts.map +1 -0
  63. package/dist/session/session-manager.js +1339 -0
  64. package/dist/session/session-manager.js.map +1 -0
  65. package/dist/utils/paths.d.ts +15 -0
  66. package/dist/utils/paths.d.ts.map +1 -0
  67. package/dist/utils/paths.js +31 -0
  68. package/dist/utils/paths.js.map +1 -0
  69. package/dist/utils/shell-output.d.ts +10 -0
  70. package/dist/utils/shell-output.d.ts.map +1 -0
  71. package/dist/utils/shell-output.js +38 -0
  72. package/dist/utils/shell-output.js.map +1 -0
  73. package/dist/utils/truncate.d.ts.map +1 -0
  74. package/dist/{harness/utils → utils}/truncate.js +27 -102
  75. package/dist/utils/truncate.js.map +1 -0
  76. package/dist/uuid.d.ts.map +1 -0
  77. package/dist/uuid.js.map +1 -0
  78. package/package.json +6 -2
  79. package/dist/harness/agent-harness.d.ts +0 -95
  80. package/dist/harness/agent-harness.d.ts.map +0 -1
  81. package/dist/harness/agent-harness.js +0 -963
  82. package/dist/harness/agent-harness.js.map +0 -1
  83. package/dist/harness/compaction/branch-summarization.d.ts +0 -53
  84. package/dist/harness/compaction/branch-summarization.d.ts.map +0 -1
  85. package/dist/harness/compaction/branch-summarization.js.map +0 -1
  86. package/dist/harness/compaction/compaction.d.ts +0 -95
  87. package/dist/harness/compaction/compaction.d.ts.map +0 -1
  88. package/dist/harness/compaction/compaction.js.map +0 -1
  89. package/dist/harness/compaction/utils.d.ts +0 -25
  90. package/dist/harness/compaction/utils.d.ts.map +0 -1
  91. package/dist/harness/compaction/utils.js.map +0 -1
  92. package/dist/harness/env/nodejs.d.ts +0 -51
  93. package/dist/harness/env/nodejs.d.ts.map +0 -1
  94. package/dist/harness/env/nodejs.js +0 -486
  95. package/dist/harness/env/nodejs.js.map +0 -1
  96. package/dist/harness/messages.d.ts.map +0 -1
  97. package/dist/harness/messages.js.map +0 -1
  98. package/dist/harness/prompt-templates.d.ts +0 -48
  99. package/dist/harness/prompt-templates.d.ts.map +0 -1
  100. package/dist/harness/prompt-templates.js +0 -233
  101. package/dist/harness/prompt-templates.js.map +0 -1
  102. package/dist/harness/session/jsonl-repo.d.ts +0 -28
  103. package/dist/harness/session/jsonl-repo.d.ts.map +0 -1
  104. package/dist/harness/session/jsonl-repo.js +0 -112
  105. package/dist/harness/session/jsonl-repo.js.map +0 -1
  106. package/dist/harness/session/jsonl-storage.d.ts +0 -33
  107. package/dist/harness/session/jsonl-storage.d.ts.map +0 -1
  108. package/dist/harness/session/jsonl-storage.js +0 -231
  109. package/dist/harness/session/jsonl-storage.js.map +0 -1
  110. package/dist/harness/session/memory-repo.d.ts +0 -18
  111. package/dist/harness/session/memory-repo.d.ts.map +0 -1
  112. package/dist/harness/session/memory-repo.js +0 -42
  113. package/dist/harness/session/memory-repo.js.map +0 -1
  114. package/dist/harness/session/memory-storage.d.ts +0 -25
  115. package/dist/harness/session/memory-storage.d.ts.map +0 -1
  116. package/dist/harness/session/memory-storage.js +0 -114
  117. package/dist/harness/session/memory-storage.js.map +0 -1
  118. package/dist/harness/session/repo-utils.d.ts +0 -11
  119. package/dist/harness/session/repo-utils.d.ts.map +0 -1
  120. package/dist/harness/session/repo-utils.js +0 -39
  121. package/dist/harness/session/repo-utils.js.map +0 -1
  122. package/dist/harness/session/session.d.ts +0 -33
  123. package/dist/harness/session/session.d.ts.map +0 -1
  124. package/dist/harness/session/session.js +0 -210
  125. package/dist/harness/session/session.js.map +0 -1
  126. package/dist/harness/session/uuid.d.ts.map +0 -1
  127. package/dist/harness/session/uuid.js.map +0 -1
  128. package/dist/harness/skills.d.ts +0 -44
  129. package/dist/harness/skills.d.ts.map +0 -1
  130. package/dist/harness/skills.js +0 -311
  131. package/dist/harness/skills.js.map +0 -1
  132. package/dist/harness/system-prompt.d.ts +0 -3
  133. package/dist/harness/system-prompt.d.ts.map +0 -1
  134. package/dist/harness/system-prompt.js +0 -30
  135. package/dist/harness/system-prompt.js.map +0 -1
  136. package/dist/harness/types.d.ts +0 -615
  137. package/dist/harness/types.d.ts.map +0 -1
  138. package/dist/harness/types.js +0 -94
  139. package/dist/harness/types.js.map +0 -1
  140. package/dist/harness/utils/shell-output.d.ts +0 -14
  141. package/dist/harness/utils/shell-output.d.ts.map +0 -1
  142. package/dist/harness/utils/shell-output.js +0 -126
  143. package/dist/harness/utils/shell-output.js.map +0 -1
  144. package/dist/harness/utils/truncate.d.ts.map +0 -1
  145. package/dist/harness/utils/truncate.js.map +0 -1
  146. /package/dist/{harness/utils → utils}/truncate.d.ts +0 -0
  147. /package/dist/{harness/session/uuid.d.ts → uuid.d.ts} +0 -0
  148. /package/dist/{harness/session/uuid.js → uuid.js} +0 -0
@@ -1,21 +1,23 @@
1
+ /**
2
+ * Context compaction for long sessions.
3
+ *
4
+ * Pure functions for compaction logic. The session manager handles I/O,
5
+ * and after compaction the session is reloaded.
6
+ */
1
7
  import { completeSimple } from "@caupulican/pi-ai";
2
8
  import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
3
- import { buildSessionContext } from "../session/session.js";
4
- import { CompactionError, err, ok } from "../types.js";
5
- import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, } from "./utils.js";
6
- function safeJsonStringify(value) {
7
- try {
8
- return JSON.stringify(value) ?? "undefined";
9
- }
10
- catch {
11
- return "[unserializable]";
12
- }
13
- }
9
+ import { buildSessionContext } from "../session/session-manager.js";
10
+ import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
11
+ /**
12
+ * Extract file operations from messages and previous compaction entries.
13
+ */
14
14
  function extractFileOperations(messages, entries, prevCompactionIndex) {
15
15
  const fileOps = createFileOps();
16
+ // Collect from previous compaction's details (if pi-generated)
16
17
  if (prevCompactionIndex >= 0) {
17
18
  const prevCompaction = entries[prevCompactionIndex];
18
19
  if (!prevCompaction.fromHook && prevCompaction.details) {
20
+ // fromHook field kept for session file compatibility
19
21
  const details = prevCompaction.details;
20
22
  if (Array.isArray(details.readFiles)) {
21
23
  for (const f of details.readFiles)
@@ -27,11 +29,19 @@ function extractFileOperations(messages, entries, prevCompactionIndex) {
27
29
  }
28
30
  }
29
31
  }
32
+ // Extract from tool calls in messages
30
33
  for (const msg of messages) {
31
34
  extractFileOpsFromMessage(msg, fileOps);
32
35
  }
33
36
  return fileOps;
34
37
  }
38
+ // ============================================================================
39
+ // Message Extraction
40
+ // ============================================================================
41
+ /**
42
+ * Extract AgentMessage from an entry if it produces one.
43
+ * Returns undefined for entries that don't contribute to LLM context.
44
+ */
35
45
  function getMessageFromEntry(entry) {
36
46
  if (entry.type === "message") {
37
47
  return entry.message;
@@ -53,16 +63,26 @@ function getMessageFromEntryForCompaction(entry) {
53
63
  }
54
64
  return getMessageFromEntry(entry);
55
65
  }
56
- /** Default compaction settings used by the harness. */
57
66
  export const DEFAULT_COMPACTION_SETTINGS = {
58
67
  enabled: true,
59
68
  reserveTokens: 16384,
60
69
  keepRecentTokens: 20000,
70
+ triggerPercent: 0.7,
61
71
  };
62
- /** Calculate total context tokens from provider usage. */
72
+ // ============================================================================
73
+ // Token calculation
74
+ // ============================================================================
75
+ /**
76
+ * Calculate total context tokens from usage.
77
+ * Uses the native totalTokens field when available, falls back to computing from components.
78
+ */
63
79
  export function calculateContextTokens(usage) {
64
80
  return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
65
81
  }
82
+ /**
83
+ * Get usage from an assistant message if available.
84
+ * Skips aborted and error messages as they don't have valid usage data.
85
+ */
66
86
  function getAssistantUsage(msg) {
67
87
  if (msg.role === "assistant" && "usage" in msg) {
68
88
  const assistantMsg = msg;
@@ -72,7 +92,9 @@ function getAssistantUsage(msg) {
72
92
  }
73
93
  return undefined;
74
94
  }
75
- /** Return usage from the last successful assistant message in session entries. */
95
+ /**
96
+ * Find the last non-aborted assistant message usage from session entries.
97
+ */
76
98
  export function getLastAssistantUsage(entries) {
77
99
  for (let i = entries.length - 1; i >= 0; i--) {
78
100
  const entry = entries[i];
@@ -92,7 +114,10 @@ function getLastAssistantUsageInfo(messages) {
92
114
  }
93
115
  return undefined;
94
116
  }
95
- /** Estimate context tokens for messages using provider usage when available. */
117
+ /**
118
+ * Estimate context tokens from messages, using the last assistant usage when available.
119
+ * If there are messages after the last usage, estimate their tokens with estimateTokens.
120
+ */
96
121
  export function estimateContextTokens(messages) {
97
122
  const usageInfo = getLastAssistantUsageInfo(messages);
98
123
  if (!usageInfo) {
@@ -119,12 +144,46 @@ export function estimateContextTokens(messages) {
119
144
  lastUsageIndex: usageInfo.index,
120
145
  };
121
146
  }
122
- /** Return whether context usage exceeds the configured compaction threshold. */
123
- export function shouldCompact(contextTokens, contextWindow, settings) {
147
+ /**
148
+ * Minimum projected space saving for the EARLY (fractional) compaction trigger to fire. Anti-thrashing
149
+ * (cost guard, #30): an early compaction whose summary would barely shrink the context (mostly recent,
150
+ * protected content) just burns a summarization call for little gain — skip it and let the context grow
151
+ * until either the saving is worthwhile or the hard (near-full) trigger forces it. Does NOT gate the
152
+ * hard trigger, so overflow is always avoided.
153
+ */
154
+ export const MIN_COMPACTION_SAVINGS = 0.12;
155
+ /**
156
+ * Check if compaction should trigger based on context usage.
157
+ *
158
+ * Two triggers:
159
+ * - HARD: context exceeds `contextWindow - reserveTokens` (near-full) or an explicit `triggerTokens`
160
+ * override — always compact (prevents overflow).
161
+ * - EARLY (fractional, cost guard): context exceeds `contextWindow * triggerPercent` — compact only if
162
+ * the summary would actually save enough (`MIN_COMPACTION_SAVINGS`), so we don't thrash for tiny gains.
163
+ */
164
+ export function shouldCompact(contextTokens, contextWindow, settings, triggerTokens) {
124
165
  if (!settings.enabled)
125
166
  return false;
126
- return contextTokens > contextWindow - settings.reserveTokens;
167
+ // Hard trigger: near-full, or a caller-supplied lower override. Always compacts (avoid overflow).
168
+ const reserveTrigger = contextWindow - settings.reserveTokens;
169
+ const hardTrigger = triggerTokens === undefined ? reserveTrigger : Math.min(reserveTrigger, triggerTokens);
170
+ if (contextTokens > hardTrigger)
171
+ return true;
172
+ // Early fractional trigger: bounds per-turn input cost on large-window models, gated by anti-thrashing.
173
+ const pct = settings.triggerPercent ?? 0;
174
+ if (pct > 0 && pct < 1) {
175
+ const fractionalTrigger = Math.floor(contextWindow * pct);
176
+ if (contextTokens > fractionalTrigger) {
177
+ // Projected saving ≈ the non-protected fraction (everything but the recent tail we keep).
178
+ const projectedSavings = contextTokens > 0 ? 1 - settings.keepRecentTokens / contextTokens : 0;
179
+ return projectedSavings >= MIN_COMPACTION_SAVINGS;
180
+ }
181
+ }
182
+ return false;
127
183
  }
184
+ // ============================================================================
185
+ // Cut point detection
186
+ // ============================================================================
128
187
  const ESTIMATED_IMAGE_CHARS = 4800;
129
188
  function estimateTextAndImageContentChars(content) {
130
189
  if (typeof content === "string") {
@@ -141,7 +200,10 @@ function estimateTextAndImageContentChars(content) {
141
200
  }
142
201
  return chars;
143
202
  }
144
- /** Estimate token count for one message using a conservative character heuristic. */
203
+ /**
204
+ * Estimate token count for a message using chars/4 heuristic.
205
+ * This is conservative (overestimates tokens).
206
+ */
145
207
  export function estimateTokens(message) {
146
208
  let chars = 0;
147
209
  switch (message.role) {
@@ -159,7 +221,7 @@ export function estimateTokens(message) {
159
221
  chars += block.thinking.length;
160
222
  }
161
223
  else if (block.type === "toolCall") {
162
- chars += block.name.length + safeJsonStringify(block.arguments).length;
224
+ chars += block.name.length + JSON.stringify(block.arguments).length;
163
225
  }
164
226
  }
165
227
  return Math.ceil(chars / 4);
@@ -181,6 +243,13 @@ export function estimateTokens(message) {
181
243
  }
182
244
  return 0;
183
245
  }
246
+ /**
247
+ * Find valid cut points: indices of user, assistant, custom, or bashExecution messages.
248
+ * Never cut at tool results (they must follow their tool call).
249
+ * When we cut at an assistant message with tool calls, its tool results follow it
250
+ * and will be kept.
251
+ * BashExecutionMessage is treated like a user message (user-initiated context).
252
+ */
184
253
  function findValidCutPoints(entries, startIndex, endIndex) {
185
254
  const cutPoints = [];
186
255
  for (let i = startIndex; i < endIndex; i++) {
@@ -204,26 +273,30 @@ function findValidCutPoints(entries, startIndex, endIndex) {
204
273
  }
205
274
  case "thinking_level_change":
206
275
  case "model_change":
207
- case "active_tools_change":
208
276
  case "compaction":
209
277
  case "branch_summary":
210
278
  case "custom":
211
279
  case "custom_message":
212
280
  case "label":
213
281
  case "session_info":
214
- case "leaf":
215
282
  break;
216
283
  }
284
+ // branch_summary and custom_message are user-role messages, valid cut points
217
285
  if (entry.type === "branch_summary" || entry.type === "custom_message") {
218
286
  cutPoints.push(i);
219
287
  }
220
288
  }
221
289
  return cutPoints;
222
290
  }
223
- /** Find the user-visible message that starts the turn containing an entry. */
291
+ /**
292
+ * Find the user message (or bashExecution) that starts the turn containing the given entry index.
293
+ * Returns -1 if no turn start found before the index.
294
+ * BashExecutionMessage is treated like a user message for turn boundaries.
295
+ */
224
296
  export function findTurnStartIndex(entries, entryIndex, startIndex) {
225
297
  for (let i = entryIndex; i >= startIndex; i--) {
226
298
  const entry = entries[i];
299
+ // branch_summary and custom_message are user-role messages, can start a turn
227
300
  if (entry.type === "branch_summary" || entry.type === "custom_message") {
228
301
  return i;
229
302
  }
@@ -236,21 +309,40 @@ export function findTurnStartIndex(entries, entryIndex, startIndex) {
236
309
  }
237
310
  return -1;
238
311
  }
239
- /** Find the compaction cut point that keeps approximately the requested recent-token budget. */
312
+ /**
313
+ * Find the cut point in session entries that keeps approximately `keepRecentTokens`.
314
+ *
315
+ * Algorithm: Walk backwards from newest, accumulating estimated message sizes.
316
+ * Stop when we've accumulated >= keepRecentTokens. Cut at that point.
317
+ *
318
+ * Can cut at user OR assistant messages (never tool results). When cutting at an
319
+ * assistant message with tool calls, its tool results come after and will be kept.
320
+ *
321
+ * Returns CutPointResult with:
322
+ * - firstKeptEntryIndex: the entry index to start keeping from
323
+ * - turnStartIndex: if cutting mid-turn, the user message that started that turn
324
+ * - isSplitTurn: whether we're cutting in the middle of a turn
325
+ *
326
+ * Only considers entries between `startIndex` and `endIndex` (exclusive).
327
+ */
240
328
  export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
241
329
  const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
242
330
  if (cutPoints.length === 0) {
243
331
  return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
244
332
  }
333
+ // Walk backwards from newest, accumulating estimated message sizes
245
334
  let accumulatedTokens = 0;
246
- let cutIndex = cutPoints[0];
335
+ let cutIndex = cutPoints[0]; // Default: keep from first message (not header)
247
336
  for (let i = endIndex - 1; i >= startIndex; i--) {
248
337
  const entry = entries[i];
249
338
  if (entry.type !== "message")
250
339
  continue;
340
+ // Estimate this message's size
251
341
  const messageTokens = estimateTokens(entry.message);
252
342
  accumulatedTokens += messageTokens;
343
+ // Check if we've exceeded the budget
253
344
  if (accumulatedTokens >= keepRecentTokens) {
345
+ // Find the closest valid cut point at or after this entry
254
346
  for (let c = 0; c < cutPoints.length; c++) {
255
347
  if (cutPoints[c] >= i) {
256
348
  cutIndex = cutPoints[c];
@@ -260,16 +352,21 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
260
352
  break;
261
353
  }
262
354
  }
355
+ // Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)
263
356
  while (cutIndex > startIndex) {
264
357
  const prevEntry = entries[cutIndex - 1];
358
+ // Stop at session header or compaction boundaries
265
359
  if (prevEntry.type === "compaction") {
266
360
  break;
267
361
  }
268
362
  if (prevEntry.type === "message") {
363
+ // Stop if we hit any message
269
364
  break;
270
365
  }
366
+ // Include this non-message entry (bash, settings change, etc.)
271
367
  cutIndex--;
272
368
  }
369
+ // Determine if this is a split turn
273
370
  const cutEntry = entries[cutIndex];
274
371
  const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
275
372
  const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
@@ -279,9 +376,9 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
279
376
  isSplitTurn: !isUserMessage && turnStartIndex !== -1,
280
377
  };
281
378
  }
282
- export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
283
-
284
- Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;
379
+ // ============================================================================
380
+ // Summarization
381
+ // ============================================================================
285
382
  const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
286
383
 
287
384
  Use this EXACT format:
@@ -352,15 +449,46 @@ Use this EXACT format:
352
449
  - [Preserve important context, add new if needed]
353
450
 
354
451
  Keep each section concise. Preserve exact file paths, function names, and error messages.`;
355
- /** Generate or update a conversation summary for compaction. */
356
- export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel) {
452
+ function createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel) {
453
+ const options = { maxTokens, signal, apiKey, headers };
454
+ if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
455
+ options.reasoning = thinkingLevel;
456
+ }
457
+ return options;
458
+ }
459
+ async function completeSummarization(model, context, options, streamFn) {
460
+ if (!streamFn) {
461
+ return completeSimple(model, context, options);
462
+ }
463
+ const stream = await streamFn(model, context, options);
464
+ return stream.result();
465
+ }
466
+ /**
467
+ * Generate a summary of the conversation using the LLM.
468
+ * If previousSummary is provided, uses the update prompt to merge.
469
+ */
470
+ export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest) {
357
471
  const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
472
+ // Use update prompt if we have a previous summary, otherwise initial prompt
358
473
  let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
359
474
  if (customInstructions) {
360
475
  basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`;
361
476
  }
477
+ // Serialize conversation to text so model doesn't try to continue it
478
+ // Convert to LLM messages first (handles custom types like bashExecution, custom, etc.)
362
479
  const llmMessages = convertToLlm(currentMessages);
363
- const conversationText = serializeConversation(llmMessages);
480
+ let conversationText = serializeConversation(llmMessages);
481
+ // Brain-curation surface 3 (opt-in, injected by the session): pre-digest old chunks locally
482
+ // before the frontier summarization call. Best-effort — failure keeps the verbatim text.
483
+ if (preDigest) {
484
+ try {
485
+ conversationText = await preDigest(conversationText, signal);
486
+ }
487
+ catch {
488
+ // verbatim fallback
489
+ }
490
+ }
491
+ // Build the prompt with conversation wrapped in tags
364
492
  let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
365
493
  if (previousSummary) {
366
494
  promptText += `<previous-summary>\n${previousSummary}\n</previous-summary>\n\n`;
@@ -373,26 +501,20 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
373
501
  timestamp: Date.now(),
374
502
  },
375
503
  ];
376
- const completionOptions = model.reasoning && thinkingLevel && thinkingLevel !== "off"
377
- ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
378
- : { maxTokens, signal, apiKey, headers };
379
- const response = await completeSimple(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions);
380
- if (response.stopReason === "aborted") {
381
- return err(new CompactionError("aborted", response.errorMessage || "Summarization aborted"));
382
- }
504
+ const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel);
505
+ const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, streamFn);
383
506
  if (response.stopReason === "error") {
384
- return err(new CompactionError("summarization_failed", `Summarization failed: ${response.errorMessage || "Unknown error"}`));
507
+ throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
385
508
  }
386
509
  const textContent = response.content
387
510
  .filter((c) => c.type === "text")
388
511
  .map((c) => c.text)
389
512
  .join("\n");
390
- return ok(textContent);
513
+ return textContent;
391
514
  }
392
- /** Prepare session entries for compaction, or return undefined when compaction is not applicable. */
393
515
  export function prepareCompaction(pathEntries, settings) {
394
- if (pathEntries.length === 0 || pathEntries[pathEntries.length - 1].type === "compaction") {
395
- return ok(undefined);
516
+ if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
517
+ return undefined;
396
518
  }
397
519
  let prevCompactionIndex = -1;
398
520
  for (let i = pathEntries.length - 1; i >= 0; i--) {
@@ -412,18 +534,21 @@ export function prepareCompaction(pathEntries, settings) {
412
534
  const boundaryEnd = pathEntries.length;
413
535
  const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;
414
536
  const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
537
+ // Get UUID of first kept entry
415
538
  const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
416
539
  if (!firstKeptEntry?.id) {
417
- return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
540
+ return undefined; // Session needs migration
418
541
  }
419
542
  const firstKeptEntryId = firstKeptEntry.id;
420
543
  const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
544
+ // Messages to summarize (will be discarded after summary)
421
545
  const messagesToSummarize = [];
422
546
  for (let i = boundaryStart; i < historyEnd; i++) {
423
547
  const msg = getMessageFromEntryForCompaction(pathEntries[i]);
424
548
  if (msg)
425
549
  messagesToSummarize.push(msg);
426
550
  }
551
+ // Messages for turn prefix summary (if splitting a turn)
427
552
  const turnPrefixMessages = [];
428
553
  if (cutPoint.isSplitTurn) {
429
554
  for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {
@@ -432,13 +557,15 @@ export function prepareCompaction(pathEntries, settings) {
432
557
  turnPrefixMessages.push(msg);
433
558
  }
434
559
  }
560
+ // Extract file operations from messages and previous compaction
435
561
  const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
562
+ // Also extract file ops from turn prefix if splitting
436
563
  if (cutPoint.isSplitTurn) {
437
564
  for (const msg of turnPrefixMessages) {
438
565
  extractFileOpsFromMessage(msg, fileOps);
439
566
  }
440
567
  }
441
- return ok({
568
+ return {
442
569
  firstKeptEntryId,
443
570
  messagesToSummarize,
444
571
  turnPrefixMessages,
@@ -447,8 +574,11 @@ export function prepareCompaction(pathEntries, settings) {
447
574
  previousSummary,
448
575
  fileOps,
449
576
  settings,
450
- });
577
+ };
451
578
  }
579
+ // ============================================================================
580
+ // Main compaction function
581
+ // ============================================================================
452
582
  const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
453
583
 
454
584
  Summarize the prefix to provide context for the retained suffix:
@@ -463,44 +593,50 @@ Summarize the prefix to provide context for the retained suffix:
463
593
  - [Information needed to understand the retained recent work]
464
594
 
465
595
  Be concise. Focus on what's needed to understand the kept suffix.`;
466
- export { serializeConversation } from "./utils.js";
467
- /** Generate compaction summary data from prepared session history. */
468
- export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel) {
596
+ /**
597
+ * Generate summaries for compaction using prepared data.
598
+ * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.
599
+ *
600
+ * @param preparation - Pre-calculated preparation from prepareCompaction()
601
+ * @param customInstructions - Optional custom focus for the summary
602
+ */
603
+ export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, preDigest) {
469
604
  const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
470
- if (!firstKeptEntryId) {
471
- return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
472
- }
605
+ // Generate summaries (can be parallel if both needed) and merge into one
473
606
  let summary;
474
607
  if (isSplitTurn && turnPrefixMessages.length > 0) {
608
+ // Generate both summaries in parallel
475
609
  const [historyResult, turnPrefixResult] = await Promise.all([
476
610
  messagesToSummarize.length > 0
477
- ? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel)
478
- : Promise.resolve(ok("No prior history.")),
479
- generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel),
611
+ ? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest)
612
+ : Promise.resolve("No prior history."),
613
+ generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn),
480
614
  ]);
481
- if (!historyResult.ok)
482
- return err(historyResult.error);
483
- if (!turnPrefixResult.ok)
484
- return err(turnPrefixResult.error);
485
- summary = `${historyResult.value}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value}`;
615
+ // Merge into single summary
616
+ summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
486
617
  }
487
618
  else {
488
- const summaryResult = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel);
489
- if (!summaryResult.ok)
490
- return err(summaryResult.error);
491
- summary = summaryResult.value;
619
+ // Just generate history summary
620
+ summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest);
492
621
  }
622
+ // Compute file lists and append to summary
493
623
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
494
624
  summary += formatFileOperations(readFiles, modifiedFiles);
495
- return ok({
625
+ if (!firstKeptEntryId) {
626
+ throw new Error("First kept entry has no UUID - session may need migration");
627
+ }
628
+ return {
496
629
  summary,
497
630
  firstKeptEntryId,
498
631
  tokensBefore,
499
632
  details: { readFiles, modifiedFiles },
500
- });
633
+ };
501
634
  }
502
- async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, signal, thinkingLevel) {
503
- const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
635
+ /**
636
+ * Generate a summary for a turn prefix (when splitting a turn).
637
+ */
638
+ async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn) {
639
+ const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); // Smaller budget for turn prefix
504
640
  const llmMessages = convertToLlm(messages);
505
641
  const conversationText = serializeConversation(llmMessages);
506
642
  const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
@@ -511,18 +647,13 @@ async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey,
511
647
  timestamp: Date.now(),
512
648
  },
513
649
  ];
514
- const response = await completeSimple(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, model.reasoning && thinkingLevel && thinkingLevel !== "off"
515
- ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
516
- : { maxTokens, signal, apiKey, headers });
517
- if (response.stopReason === "aborted") {
518
- return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted"));
519
- }
650
+ const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel), streamFn);
520
651
  if (response.stopReason === "error") {
521
- return err(new CompactionError("summarization_failed", `Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`));
652
+ throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
522
653
  }
523
- return ok(response.content
654
+ return response.content
524
655
  .filter((c) => c.type === "text")
525
656
  .map((c) => c.text)
526
- .join("\n"));
657
+ .join("\n");
527
658
  }
528
659
  //# sourceMappingURL=compaction.js.map