@caupulican/pi-agent-core 0.81.39 → 0.81.41

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 (57) hide show
  1. package/README.md +10 -5
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +77 -89
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/compaction/branch-summarization.d.ts +4 -2
  6. package/dist/compaction/branch-summarization.d.ts.map +1 -1
  7. package/dist/compaction/branch-summarization.js +4 -0
  8. package/dist/compaction/branch-summarization.js.map +1 -1
  9. package/dist/compaction/compaction.d.ts +32 -1
  10. package/dist/compaction/compaction.d.ts.map +1 -1
  11. package/dist/compaction/compaction.js +172 -101
  12. package/dist/compaction/compaction.js.map +1 -1
  13. package/dist/compaction/extraction.d.ts.map +1 -1
  14. package/dist/compaction/extraction.js +1 -1
  15. package/dist/compaction/extraction.js.map +1 -1
  16. package/dist/compaction/loop.d.ts +1 -1
  17. package/dist/compaction/loop.d.ts.map +1 -1
  18. package/dist/compaction/loop.js +24 -7
  19. package/dist/compaction/loop.js.map +1 -1
  20. package/dist/compaction/utils.d.ts.map +1 -1
  21. package/dist/compaction/utils.js +3 -1
  22. package/dist/compaction/utils.js.map +1 -1
  23. package/dist/compaction/verification.d.ts +11 -0
  24. package/dist/compaction/verification.d.ts.map +1 -1
  25. package/dist/compaction/verification.js +93 -11
  26. package/dist/compaction/verification.js.map +1 -1
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/proxy.d.ts +1 -1
  32. package/dist/proxy.d.ts.map +1 -1
  33. package/dist/proxy.js +1 -0
  34. package/dist/proxy.js.map +1 -1
  35. package/dist/reliability/classifier.d.ts.map +1 -1
  36. package/dist/reliability/classifier.js +3 -3
  37. package/dist/reliability/classifier.js.map +1 -1
  38. package/dist/session/session-manager.d.ts +18 -6
  39. package/dist/session/session-manager.d.ts.map +1 -1
  40. package/dist/session/session-manager.js +68 -61
  41. package/dist/session/session-manager.js.map +1 -1
  42. package/dist/tool-failure-memory.d.ts +35 -0
  43. package/dist/tool-failure-memory.d.ts.map +1 -0
  44. package/dist/tool-failure-memory.js +290 -0
  45. package/dist/tool-failure-memory.js.map +1 -0
  46. package/dist/types.d.ts +5 -1
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/types.js.map +1 -1
  49. package/dist/usage.d.ts +5 -0
  50. package/dist/usage.d.ts.map +1 -1
  51. package/dist/usage.js +32 -0
  52. package/dist/usage.js.map +1 -1
  53. package/dist/uuid.d.ts +1 -1
  54. package/dist/uuid.d.ts.map +1 -1
  55. package/dist/uuid.js +1 -49
  56. package/dist/uuid.js.map +1 -1
  57. package/package.json +2 -2
@@ -4,12 +4,13 @@
4
4
  * Pure functions for compaction logic. The session manager handles I/O,
5
5
  * and after compaction the session is reloaded.
6
6
  */
7
- import { completeSimple } from "@caupulican/pi-ai";
7
+ import { completeSimple, uuidv7 } from "@caupulican/pi-ai";
8
8
  import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
9
9
  import { buildSessionContext } from "../session/session-manager.js";
10
+ import { addUsage, combineUsage, createEmptyUsage } from "../usage.js";
10
11
  import { extractCompactionFacts, renderFactsBlock } from "./extraction.js";
11
12
  import { computeFileLists, createFileOps, extractFileOpsFromMessage, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
12
- import { buildRetryPrompt, deterministicallyFillSummaryGaps, isCompactionSummaryStructurallyUsable, verifySummary, } from "./verification.js";
13
+ import { buildRetryPrompt, CompactionVerificationError, deterministicallyFillSummaryGaps, isCompactionSummaryStructurallyUsable, verifySummary, } from "./verification.js";
13
14
  /**
14
15
  * Extract file operations from messages and previous compaction entries.
15
16
  */
@@ -65,6 +66,60 @@ function getMessageFromEntryForCompaction(entry) {
65
66
  }
66
67
  return getMessageFromEntry(entry);
67
68
  }
69
+ /**
70
+ * Carry failed LLM verification attempts into the result that the retry ladder eventually applies.
71
+ * Only bounded numeric/check identifiers are persisted in details; raw facts stay in the in-memory reports.
72
+ */
73
+ export function mergeCompactionVerificationReports(result, reports) {
74
+ if (reports.length === 0)
75
+ return result;
76
+ const combinedReports = [
77
+ ...reports.map(cloneVerificationReport),
78
+ ...(result.verificationGateFailures ?? []).map(cloneVerificationReport),
79
+ ];
80
+ result.verificationGateFailures = combinedReports;
81
+ if (result.details === undefined || isPlainRecord(result.details)) {
82
+ const details = result.details ?? {};
83
+ result.details = {
84
+ ...details,
85
+ verificationGateFailures: combinedReports.length,
86
+ verificationGateChecks: aggregateVerificationChecks(combinedReports),
87
+ };
88
+ }
89
+ return result;
90
+ }
91
+ function cloneVerificationReport(report) {
92
+ return {
93
+ ok: report.ok,
94
+ failures: report.failures.map((failure) => ({ ...failure })),
95
+ };
96
+ }
97
+ function isPlainRecord(value) {
98
+ if (!value || typeof value !== "object" || Array.isArray(value))
99
+ return false;
100
+ const prototype = Object.getPrototypeOf(value);
101
+ return prototype === Object.prototype || prototype === null;
102
+ }
103
+ function aggregateVerificationChecks(reports) {
104
+ const checks = new Map();
105
+ for (const report of reports) {
106
+ for (const failure of report.failures) {
107
+ const current = checks.get(failure.check) ?? { failures: 0 };
108
+ current.failures++;
109
+ if (failure.score !== undefined && Number.isFinite(failure.score)) {
110
+ current.minScore = Math.min(current.minScore ?? failure.score, failure.score);
111
+ current.maxScore = Math.max(current.maxScore ?? failure.score, failure.score);
112
+ }
113
+ if (failure.threshold !== undefined && Number.isFinite(failure.threshold)) {
114
+ current.threshold = failure.threshold;
115
+ }
116
+ if (failure.comparator)
117
+ current.comparator = failure.comparator;
118
+ checks.set(failure.check, current);
119
+ }
120
+ }
121
+ return Object.fromEntries(checks);
122
+ }
68
123
  export const DEFAULT_COMPACTION_SETTINGS = {
69
124
  enabled: true,
70
125
  reserveTokens: 16384,
@@ -108,20 +163,33 @@ export function getLastAssistantUsage(entries) {
108
163
  }
109
164
  return undefined;
110
165
  }
111
- function getLastAssistantUsageInfo(messages) {
112
- for (let i = messages.length - 1; i >= 0; i--) {
113
- const usage = getAssistantUsage(messages[i]);
114
- if (usage)
115
- return { usage, index: i };
166
+ /**
167
+ * Find the newest assistant usage that still describes the current message prefix.
168
+ *
169
+ * Compaction can insert a newer summary before retained older messages. Usage recorded by an
170
+ * assistant before that inserted message describes the pre-compaction prefix and must not anchor
171
+ * the rebuilt context estimate. Walking forward lets the newest timestamp in the prefix invalidate
172
+ * those stale usage blocks while still accepting the first response produced after compaction.
173
+ */
174
+ export function getApplicableAssistantUsageInfo(messages) {
175
+ let latestPrefixTimestamp = Number.NEGATIVE_INFINITY;
176
+ let usageInfo;
177
+ for (let i = 0; i < messages.length; i++) {
178
+ const message = messages[i];
179
+ const usage = getAssistantUsage(message);
180
+ if (usage && message.timestamp >= latestPrefixTimestamp) {
181
+ usageInfo = { usage, index: i };
182
+ }
183
+ latestPrefixTimestamp = Math.max(latestPrefixTimestamp, message.timestamp);
116
184
  }
117
- return undefined;
185
+ return usageInfo;
118
186
  }
119
187
  /**
120
188
  * Estimate context tokens from messages, using the last assistant usage when available.
121
189
  * If there are messages after the last usage, estimate their tokens with estimateTokens.
122
190
  */
123
191
  export function estimateContextTokens(messages) {
124
- const usageInfo = getLastAssistantUsageInfo(messages);
192
+ const usageInfo = getApplicableAssistantUsageInfo(messages);
125
193
  if (!usageInfo) {
126
194
  let estimated = 0;
127
195
  for (const message of messages) {
@@ -418,11 +486,16 @@ Same section order. Verification checklist (the verifier checks exactly these ch
418
486
 
419
487
  Budget: ~{BUDGET} tokens.`;
420
488
  function createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel) {
421
- // SUMMARIZATION_SYSTEM_PROMPT is static and the compaction retry ladder can resend a
422
- // near-identical prefix (same conversation/facts block) within one attempt let the provider
423
- // cache it so a retry only pays for the variable tail. Matches the repo's default ("short")
424
- // exactly; set explicitly so the intent isn't silently dependent on the provider default.
425
- const options = { maxTokens, signal, apiKey, headers, cacheRetention: "short" };
489
+ // Summaries are one-shot prompts. A fresh affinity identity prevents them from contaminating the
490
+ // foreground continuation cache, while "none" prevents cache writes that cannot be reused.
491
+ const options = {
492
+ maxTokens,
493
+ signal,
494
+ apiKey,
495
+ headers,
496
+ cacheRetention: "none",
497
+ sessionId: uuidv7(),
498
+ };
426
499
  if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
427
500
  options.reasoning = thinkingLevel;
428
501
  }
@@ -467,7 +540,11 @@ async function prepareSummarizationConversationText(currentMessages, preDigest,
467
540
  * the same message span across multiple attempts (a verification-gate retry), compute this once
468
541
  * via {@link prepareSummarizationConversationText} and pass it to every attempt.
469
542
  */
470
- export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest, factsBlock = "verification demands:\nfiles-modified-recall (must appear in ## Files):\nfiles-read-recall (must appear in ## Files, containment threshold applies):\nworking-set-recall (must appear in ## Working Set):\nopen-errors-recall (must appear in ## Open Problems):\nactions-recall (must appear in ## Done):\nmandatory-rules-recall (must appear in ### Mandatory Rules):\nactive-task-containment (must appear in ## Active Task):\ncancelled-work-dropped (must NOT appear outside ### Mandatory Rules):", chunked = false, precomputedConversationText) {
543
+ export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest, factsBlock = "verification demands:\nfiles-modified-recall (must appear in ## Files):\nfiles-read-recall (must appear as exact paths in ## Files, path recall threshold applies):\nworking-set-recall (must appear in ## Working Set):\nopen-errors-recall (must appear in ## Open Problems):\nactions-recall (must appear in ## Done):\nmandatory-rules-recall (must appear in ### Mandatory Rules):\nactive-task-containment (must appear in ## Active Task):\ncancelled-work-dropped (must NOT appear outside ### Mandatory Rules):", chunked = false, precomputedConversationText) {
544
+ return (await generateSummaryWithUsage(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest, factsBlock, chunked, precomputedConversationText)).text;
545
+ }
546
+ export async function generateSummaryWithUsage(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest, factsBlock = "verification demands:\nfiles-modified-recall (must appear in ## Files):\nfiles-read-recall (must appear as exact paths in ## Files, path recall threshold applies):\nworking-set-recall (must appear in ## Working Set):\nopen-errors-recall (must appear in ## Open Problems):\nactions-recall (must appear in ## Done):\nmandatory-rules-recall (must appear in ### Mandatory Rules):\nactive-task-containment (must appear in ## Active Task):\ncancelled-work-dropped (must NOT appear outside ### Mandatory Rules):", chunked = false, precomputedConversationText) {
547
+ const usage = createEmptyUsage();
471
548
  const summaryBudget = getSummaryBudget(reserveTokens, model, factsBlock);
472
549
  const maxTokens = summaryBudget;
473
550
  let promptSuffix = fillPromptTemplate(previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT, factsBlock, summaryBudget);
@@ -483,7 +560,7 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
483
560
  if (!chunked) {
484
561
  throw new Error("input-overflow: summarization request exceeds summarizer window");
485
562
  }
486
- conversationText = await summarizeChunks(conversationText, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, inputBound, previousSummary, promptSuffix);
563
+ conversationText = await summarizeChunks(conversationText, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, inputBound, previousSummary, promptSuffix, usage);
487
564
  }
488
565
  const promptText = buildSummarizationPrompt(conversationText, previousSummary, promptSuffix);
489
566
  if (estimateStringTokens(promptText) > inputBound) {
@@ -499,6 +576,7 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
499
576
  },
500
577
  ],
501
578
  }, createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel), streamFn);
579
+ addUsage(usage, response.usage);
502
580
  if (response.stopReason === "error") {
503
581
  throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
504
582
  }
@@ -507,7 +585,7 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
507
585
  if (response.stopReason === "length") {
508
586
  throw new Error("summary-length-stop: summarizer hit its output cap before completing the checkpoint");
509
587
  }
510
- return truncateSummaryToBudget(extractTextContent(response), summaryBudget);
588
+ return { text: truncateSummaryToBudget(extractTextContent(response), summaryBudget), usage };
511
589
  }
512
590
  function fillPromptTemplate(template, factsBlock, budget) {
513
591
  return template.replaceAll("{FACTS_BLOCK}", factsBlock).replaceAll("{BUDGET}", String(budget));
@@ -569,10 +647,10 @@ export function getChunkSummarizationTokenBudget(inputBound) {
569
647
  export function buildChunkSummarizationPrompt(chunk, index, total) {
570
648
  return `<conversation-chunk index="${index}" total="${total}">\n${chunk}\n</conversation-chunk>\n\nSummarize this chunk for a later checkpoint merge. Preserve exact file paths, commands, errors, user prohibitions, and active work. Output concise notes only.`;
571
649
  }
572
- async function summarizeChunks(conversationText, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, inputBound, previousSummary, promptSuffix) {
650
+ async function summarizeChunks(conversationText, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, inputBound, previousSummary, promptSuffix, usage) {
573
651
  let reducedText = conversationText;
574
652
  for (let pass = 0; pass < 3; pass++) {
575
- const summary = await summarizeChunkPass(reducedText, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, inputBound);
653
+ const summary = await summarizeChunkPass(reducedText, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, inputBound, usage);
576
654
  if (estimateStringTokens(buildSummarizationPrompt(summary, previousSummary, promptSuffix)) <= inputBound) {
577
655
  return summary;
578
656
  }
@@ -580,7 +658,7 @@ async function summarizeChunks(conversationText, model, maxTokens, apiKey, heade
580
658
  }
581
659
  throw new Error("input-overflow: chunked summarization merge still exceeds summarizer window");
582
660
  }
583
- async function summarizeChunkPass(conversationText, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, inputBound) {
661
+ async function summarizeChunkPass(conversationText, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, inputBound, usage) {
584
662
  const maxChunkTokens = getChunkSummarizationTokenBudget(inputBound);
585
663
  const maxChunkChars = Math.max(1, maxChunkTokens * 4);
586
664
  const chunks = splitText(conversationText, maxChunkChars);
@@ -597,6 +675,7 @@ async function summarizeChunkPass(conversationText, model, maxTokens, apiKey, he
597
675
  },
598
676
  ],
599
677
  }, createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel), streamFn);
678
+ addUsage(usage, response.usage);
600
679
  if (response.stopReason === "error") {
601
680
  throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
602
681
  }
@@ -744,6 +823,40 @@ Summarize the prefix to provide context for the retained suffix:
744
823
  - [Information needed to understand the retained recent work]
745
824
 
746
825
  Be concise. Focus on what's needed to understand the kept suffix.`;
826
+ async function generateVerifiedSummary(options) {
827
+ let retryInstructions = options.customInstructions;
828
+ const usage = createEmptyUsage();
829
+ const verificationGateFailures = [];
830
+ const precomputedConversationText = await prepareSummarizationConversationText(options.messages, options.preDigest, options.signal);
831
+ for (let attempt = 0; attempt < 2; attempt++) {
832
+ const generated = await generateSummaryWithUsage(options.messages, options.model, options.reserveTokens, options.apiKey, options.headers, options.signal, retryInstructions, options.previousSummary, options.thinkingLevel, options.streamFn, options.preDigest, options.factsBlock, options.chunked, precomputedConversationText);
833
+ addUsage(usage, generated.usage);
834
+ const summary = generated.text;
835
+ const verification = verifySummary(summary, options.facts);
836
+ if (verification.ok) {
837
+ return { summary, usage, verification, verificationGateFailures, deterministicGapFills: 0 };
838
+ }
839
+ verificationGateFailures.push(verification);
840
+ if (!isCompactionSummaryStructurallyUsable(summary)) {
841
+ if (attempt >= 1)
842
+ throw new CompactionVerificationError(verificationGateFailures);
843
+ retryInstructions = buildRetryPrompt(verification, summary);
844
+ continue;
845
+ }
846
+ const filled = deterministicallyFillSummaryGaps(summary, options.facts);
847
+ if (filled.verification.ok) {
848
+ return {
849
+ summary: filled.summary,
850
+ usage,
851
+ verification: filled.verification,
852
+ verificationGateFailures,
853
+ deterministicGapFills: filled.changed ? 1 : 0,
854
+ };
855
+ }
856
+ throw new CompactionVerificationError(verificationGateFailures);
857
+ }
858
+ throw new CompactionVerificationError(verificationGateFailures);
859
+ }
747
860
  /**
748
861
  * Generate summaries for compaction using prepared data.
749
862
  * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.
@@ -764,95 +877,50 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
764
877
  delegatedWorkerFacts: [],
765
878
  };
766
879
  const factsBlock = renderFactsBlock(facts);
767
- let verification;
768
- const verificationGateFailures = [];
769
- let deterministicGapFills = 0;
770
- let summary = "";
771
- if (isSplitTurn && messagesToSummarize.length > 0) {
772
- let historySummary = "No prior history.";
773
- let historyInstructions = customInstructions;
774
- // Computed once and reused across retry attempts below: `messagesToSummarize` and `preDigest`
775
- // are identical on every attempt (only the retry instructions change), and `preDigest` makes
776
- // real model calls — re-running serialize+preDigest per attempt would resend/re-summarize the
777
- // unchanged span for no benefit.
778
- const precomputedConversationText = await prepareSummarizationConversationText(messagesToSummarize, preDigest, signal);
779
- for (let attempt = 0; attempt < 2; attempt++) {
780
- historySummary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, historyInstructions, previousSummary, thinkingLevel, streamFn, preDigest, factsBlock, executionOptions?.chunked ?? false, precomputedConversationText);
781
- verification = verifySummary(historySummary, facts);
782
- if (verification.ok) {
783
- break;
784
- }
785
- if (!isCompactionSummaryStructurallyUsable(historySummary)) {
786
- if (attempt >= 1) {
787
- throw new Error(`gate-failed: ${formatVerificationFailures(verification)}`);
788
- }
789
- historyInstructions = buildRetryPrompt(verification, historySummary);
790
- continue;
791
- }
792
- verificationGateFailures.push(verification);
793
- const filled = deterministicallyFillSummaryGaps(historySummary, facts);
794
- if (filled.verification.ok) {
795
- historySummary = filled.summary;
796
- verification = filled.verification;
797
- if (filled.changed)
798
- deterministicGapFills++;
799
- break;
800
- }
801
- throw new Error(`gate-failed: ${formatVerificationFailures(filled.verification)}`);
802
- }
803
- const turnPrefixSummary = await generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn);
804
- summary = `${historySummary}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixSummary}`;
805
- }
806
- else {
807
- let customSummaryInstructions = customInstructions;
808
- // See the isSplitTurn branch above: same span/preDigest across attempts, computed once.
809
- const precomputedConversationText = await prepareSummarizationConversationText(messagesToSummarize, preDigest, signal);
810
- for (let attempt = 0; attempt < 2; attempt++) {
811
- summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customSummaryInstructions, previousSummary, thinkingLevel, streamFn, preDigest, factsBlock, executionOptions?.chunked ?? false, precomputedConversationText);
812
- verification = verifySummary(summary, facts);
813
- if (verification.ok) {
814
- break;
815
- }
816
- if (!isCompactionSummaryStructurallyUsable(summary)) {
817
- if (attempt >= 1) {
818
- throw new Error(`gate-failed: ${formatVerificationFailures(verification)}`);
819
- }
820
- customSummaryInstructions = buildRetryPrompt(verification, summary);
821
- continue;
822
- }
823
- verificationGateFailures.push(verification);
824
- const filled = deterministicallyFillSummaryGaps(summary, facts);
825
- if (filled.verification.ok) {
826
- summary = filled.summary;
827
- verification = filled.verification;
828
- if (filled.changed)
829
- deterministicGapFills++;
830
- break;
831
- }
832
- throw new Error(`gate-failed: ${formatVerificationFailures(filled.verification)}`);
833
- }
880
+ const verified = isSplitTurn && messagesToSummarize.length === 0
881
+ ? undefined
882
+ : await generateVerifiedSummary({
883
+ messages: messagesToSummarize,
884
+ model,
885
+ reserveTokens: settings.reserveTokens,
886
+ apiKey,
887
+ headers,
888
+ signal,
889
+ customInstructions,
890
+ previousSummary,
891
+ thinkingLevel,
892
+ streamFn,
893
+ preDigest,
894
+ facts,
895
+ factsBlock,
896
+ chunked: executionOptions?.chunked ?? false,
897
+ });
898
+ let summary = verified?.summary ?? "No prior history.";
899
+ let summaryUsage = verified?.usage;
900
+ if (isSplitTurn) {
901
+ const turnPrefix = await generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn);
902
+ summary = `${summary}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefix.text}`;
903
+ summaryUsage = combineUsage(summaryUsage, turnPrefix.usage);
834
904
  }
835
905
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
836
906
  if (!firstKeptEntryId) {
837
907
  throw new Error("First kept entry has no UUID - session may need migration");
838
908
  }
839
- return {
909
+ return mergeCompactionVerificationReports({
840
910
  summary,
841
911
  firstKeptEntryId,
842
912
  tokensBefore,
913
+ usage: summaryUsage,
843
914
  details: {
844
915
  readFiles,
845
916
  modifiedFiles,
846
- verificationGateFailures: verificationGateFailures.length,
847
- deterministicGapFills,
917
+ verificationGateFailures: 0,
918
+ deterministicGapFills: verified?.deterministicGapFills ?? 0,
848
919
  },
849
- verification,
850
- verificationGateFailures,
851
- deterministicGapFills,
852
- };
853
- }
854
- function formatVerificationFailures(verification) {
855
- return verification.failures.map((failure) => `${failure.check}: ${failure.detail}`).join(", ");
920
+ verification: verified?.verification,
921
+ verificationGateFailures: [],
922
+ deterministicGapFills: verified?.deterministicGapFills ?? 0,
923
+ }, verified?.verificationGateFailures ?? []);
856
924
  }
857
925
  export function createDeterministicCompaction(preparation) {
858
926
  const { firstKeptEntryId, tokensBefore, fileOps, facts } = preparation;
@@ -945,9 +1013,12 @@ async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey,
945
1013
  if (response.stopReason === "error") {
946
1014
  throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
947
1015
  }
948
- return response.content
949
- .filter((c) => c.type === "text")
950
- .map((c) => c.text)
951
- .join("\n");
1016
+ return {
1017
+ text: response.content
1018
+ .filter((c) => c.type === "text")
1019
+ .map((c) => c.text)
1020
+ .join("\n"),
1021
+ usage: response.usage,
1022
+ };
952
1023
  }
953
1024
  //# sourceMappingURL=compaction.js.map