fix(compaction): retain audited facts when summaries exceed cap (#128968)

* fix(compaction): retain audited facts under summary cap

* fix(compaction): retain bounded ask context

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
This commit is contained in:
RoboClaw
2026-08-25 03:44:07 -07:00
committed by GitHub
parent a36ed8529b
commit 97cd06b946
3 changed files with 426 additions and 19 deletions
@@ -1,6 +1,7 @@
/** Quality contract, fallback, and audit helpers for compaction safeguard summaries. */
import { localeLowercasePreservingWhitespace } from "@openclaw/normalization-core/string-coerce";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { extractKeywords, isQueryStopWordToken } from "../../memory-host-sdk/query.js";
import type { CompactionSummarizationInstructions } from "../compaction.js";
import { wrapUntrustedPromptDataBlock } from "../sanitize-for-prompt.js";
@@ -18,6 +19,7 @@ const REQUIRED_SUMMARY_SECTIONS = [
"## Pending user asks",
"## Exact identifiers",
] as const;
const QUALITY_PROTECTED_SECTION_START = 3;
const STRICT_EXACT_IDENTIFIERS_INSTRUCTION =
"For ## Exact identifiers, preserve literal values exactly as seen (IDs, URLs, file paths, ports, hashes, dates, times).";
const POLICY_OFF_EXACT_IDENTIFIERS_INSTRUCTION =
@@ -93,6 +95,128 @@ function hasRequiredSummarySections(summary: string): boolean {
return true;
}
type SummaryQualityRetentionPlan = {
minimumChars: number;
render: (maxChars: number) => string | null;
};
function parseRequiredSummarySectionContents(summary: string): string[] | null {
const contents = REQUIRED_SUMMARY_SECTIONS.map(() => new Array<string>());
const preamble: string[] = [];
let sectionIndex = -1;
for (const line of summary.split(/\r?\n/u)) {
const nextHeading = REQUIRED_SUMMARY_SECTIONS[sectionIndex + 1];
if (nextHeading && line.trim() === nextHeading) {
sectionIndex += 1;
continue;
}
(sectionIndex < 0 ? preamble : contents[sectionIndex])?.push(line);
}
if (sectionIndex !== REQUIRED_SUMMARY_SECTIONS.length - 1) {
return null;
}
contents[0]?.unshift(...preamble);
return contents.map((lines) => lines.join("\n").trim());
}
/** Plan truncation that keeps audit-required headings, pending asks, and exact identifiers. */
export function createSummaryQualityRetentionPlan(
summary: string,
truncatedMarker: string,
params: {
auditSummary?: string;
identifiers: string[];
latestAsk: string | null;
requiredAskContext?: string;
identifierPolicy?: CompactionSummarizationInstructions["identifierPolicy"];
},
): SummaryQualityRetentionPlan | null {
const contents = parseRequiredSummarySectionContents(summary);
if (!contents) {
return null;
}
const enforceIdentifiers = (params.identifierPolicy ?? "strict") === "strict";
const auditSummary = params.auditSummary ?? summary;
if (
enforceIdentifiers &&
params.identifiers.some((identifier) => !summaryIncludesIdentifier(auditSummary, identifier))
) {
return null;
}
if (!hasAskOverlap(auditSummary, params.latestAsk)) {
return null;
}
const pendingAsk = contents[QUALITY_PROTECTED_SECTION_START] ?? "";
const requiredAskContext = params.requiredAskContext?.trim() ?? "";
const exactIdentifiers = contents[QUALITY_PROTECTED_SECTION_START + 1] ?? "";
const missingIdentifiers = enforceIdentifiers
? params.identifiers.filter(
(identifier) => !summaryIncludesIdentifier(exactIdentifiers, identifier),
)
: [];
const protectedContents = [
[
pendingAsk,
requiredAskContext && !pendingAsk.includes(requiredAskContext) ? requiredAskContext : "",
]
.filter(Boolean)
.join("\n"),
[exactIdentifiers, ...missingIdentifiers].filter(Boolean).join("\n"),
];
const marker = truncatedMarker.trim();
const protectedBlocks = REQUIRED_SUMMARY_SECTIONS.slice(QUALITY_PROTECTED_SECTION_START).map(
(heading, index) => {
const content = protectedContents[index];
return content ? `${heading}\n${content}` : heading;
},
);
const optionalHeadings = REQUIRED_SUMMARY_SECTIONS.slice(0, QUALITY_PROTECTED_SECTION_START);
const optionalContents = contents.slice(0, QUALITY_PROTECTED_SECTION_START);
const optionalScaffolds = optionalHeadings.map((heading, index) =>
optionalContents[index] ? `${heading}\n` : heading,
);
const minimumSummary = [...optionalScaffolds, marker, ...protectedBlocks].join("\n\n");
return {
minimumChars: minimumSummary.length,
render(maxChars) {
const bodyHasRequiredAskContext = !requiredAskContext || summary.includes(requiredAskContext);
const bodyHasIdentifiers =
!enforceIdentifiers ||
params.identifiers.every((identifier) => summaryIncludesIdentifier(summary, identifier));
if (summary.length <= maxChars && bodyHasRequiredAskContext && bodyHasIdentifiers) {
return summary;
}
if (maxChars < minimumSummary.length) {
return null;
}
const contentBudget = maxChars - minimumSummary.length;
const totalContentChars = optionalContents.reduce(
(total, content) => total + content.length,
0,
);
const allocations = optionalContents.map((content) =>
totalContentChars > 0
? Math.floor((contentBudget * content.length) / totalContentChars)
: 0,
);
let remainder = contentBudget - allocations.reduce((total, chars) => total + chars, 0);
for (const [index, content] of optionalContents.entries()) {
const allocation = allocations[index] ?? 0;
const extra = Math.min(remainder, Math.max(0, content.length - allocation));
allocations[index] = allocation + extra;
remainder -= extra;
}
const optionalBlocks = optionalHeadings.map((heading, index) => {
const content = truncateUtf16Safe(optionalContents[index] ?? "", allocations[index] ?? 0);
return content ? `${heading}\n${content}` : heading;
});
return [...optionalBlocks, marker, ...protectedBlocks].join("\n\n");
},
};
}
/** Return a structured fallback summary when model output is missing/invalid. */
export function buildStructuredFallbackSummary(previousSummary: string | undefined): string {
const trimmedPreviousSummary = previousSummary?.trim() ?? "";
@@ -174,24 +298,36 @@ function tokenizeAskOverlapText(text: string): string[] {
.filter((token) => token.length > 0);
}
function hasAskOverlap(summary: string, latestAsk: string | null): boolean {
function resolveAskOverlapRequirement(latestAsk: string | null): {
tokens: string[];
requiredMatches: number;
} | null {
if (!latestAsk) {
return true;
return null;
}
const askTokens = uniqueStrings(tokenizeAskOverlapText(latestAsk)).slice(
0,
MAX_ASK_OVERLAP_TOKENS,
);
if (askTokens.length === 0) {
return true;
return null;
}
const meaningfulAskTokens = askTokens.filter(
(token) => token.length > 1 && !isQueryStopWordToken(token),
);
const tokensToCheck = meaningfulAskTokens.length > 0 ? meaningfulAskTokens : askTokens;
const summaryTokens = new Set(tokenizeAskOverlapText(summary));
const overlapCount = tokensToCheck.filter((token) => summaryTokens.has(token)).length;
const requiredMatches = tokensToCheck.length >= MIN_ASK_OVERLAP_TOKENS_FOR_DOUBLE_MATCH ? 2 : 1;
return { tokens: tokensToCheck, requiredMatches };
}
function hasAskOverlap(summary: string, latestAsk: string | null): boolean {
const requirement = resolveAskOverlapRequirement(latestAsk);
if (!requirement) {
return true;
}
const summaryTokens = new Set(tokenizeAskOverlapText(summary));
const overlapCount = requirement.tokens.filter((token) => summaryTokens.has(token)).length;
const { requiredMatches } = requirement;
return overlapCount >= requiredMatches;
}
@@ -625,6 +625,86 @@ describe("compaction-safeguard summary budgets", () => {
expect(capped).toContain("<read-files>");
expect(capped).toContain("## Session Startup");
});
it("moves split-turn quality facts into a short body before suffix pressure", () => {
const latestAsk = "delete production only after verified backup";
const carriedIdentifier = "/tmp/carried-forward.log";
const identifier = "/tmp/split-turn-short-body.log";
const body = [
"## Decisions",
"Keep current flow.",
"## Open TODOs",
"None.",
"## Constraints/Rules",
"Preserve exact context.",
"## Pending user asks",
"Continue the active work.",
"## Exact identifiers",
carriedIdentifier,
].join("\n");
const suffix = `\n\n**Turn Context (split turn):**\n${latestAsk}\n${identifier}\n${"z".repeat(
MAX_COMPACTION_SUMMARY_CHARS,
)}`;
const auditSummary = `${body}${suffix}`;
const finalized = requireRecord(
budgetCompactionSummary(body, suffix, MAX_COMPACTION_SUMMARY_CHARS, {
auditSummary,
identifiers: [identifier],
latestAsk,
requiredAskContext: latestAsk,
identifierPolicy: "strict",
}),
);
if (typeof finalized.summary !== "string" || typeof finalized.structuralSummary !== "string") {
throw new Error("expected finalized summary strings");
}
const summary = finalized.summary;
const structuralSummary = finalized.structuralSummary;
expect(summary.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS);
expect(structuralSummary).toContain(latestAsk);
expect(structuralSummary).toContain(identifier);
expect(structuralSummary).toContain(carriedIdentifier);
expect(auditSummaryQuality({ summary, identifiers: [identifier], latestAsk }).ok).toBe(true);
});
it("moves a normal latest ask out of prose that final budgeting trims", () => {
const latestAsk = "delete production only after verified backup";
const identifier = "/tmp/normal-turn-retention.log";
const body = [
"## Decisions",
latestAsk,
"x".repeat(MAX_COMPACTION_SUMMARY_CHARS),
"## Open TODOs",
"None.",
"## Constraints/Rules",
"Preserve exact context.",
"## Pending user asks",
"Continue the active work.",
"## Exact identifiers",
identifier,
].join("\n");
const finalized = requireRecord(
budgetCompactionSummary(body, "", MAX_COMPACTION_SUMMARY_CHARS, {
auditSummary: body,
identifiers: [identifier],
latestAsk,
requiredAskContext: latestAsk,
identifierPolicy: "strict",
}),
);
if (typeof finalized.summary !== "string" || typeof finalized.structuralSummary !== "string") {
throw new Error("expected finalized summary strings");
}
expect(finalized.summary.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS);
expect(finalized.structuralSummary).toContain(
`## Pending user asks\nContinue the active work.\n${latestAsk}`,
);
expect(
auditSummaryQuality({ summary: finalized.summary, identifiers: [identifier], latestAsk }).ok,
).toBe(true);
});
});
describe("computeAdaptiveChunkRatio", () => {
@@ -2012,7 +2092,7 @@ describe("compaction-safeguard recent-turn preservation", () => {
expect(warning).not.toContain(sensitiveSentinel);
});
it("rejects a summary whose finalized bytes fail the quality audit", async () => {
it("preserves audit-required tail sections when an earlier section exhausts the budget", async () => {
mockSummarizeInStages.mockReset();
const latestAsk = "preserve the pending deployment status";
const identifier = "/tmp/compaction-final-audit.log";
@@ -2058,12 +2138,113 @@ describe("compaction-safeguard recent-turn preservation", () => {
const { result } = await runCompactionScenario({ sessionManager, event, apiKey: "test-key" });
const summary = expectCompactionResult(result).summary;
expect(summary.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS);
expect(summary).toContain(SUMMARY_TRUNCATED_MARKER.trim());
expect(summary).toContain("## Open TODOs");
expect(summary).toContain("## Constraints/Rules");
expect(summary).toContain(`## Pending user asks\n${latestAsk}`);
expect(summary).toContain(`## Exact identifiers\n${identifier}`);
expect(mockSummarizeInStages).toHaveBeenCalledTimes(1);
expect(consumeCompactionSafeguardCancelReason(sessionManager)).toBeNull();
});
it("fails closed when audit-required tail sections cannot fit the artifact cap", async () => {
mockSummarizeInStages.mockReset();
const latestAsk = "preserve the pending deployment status";
const identifier = `https://example.com/${"a".repeat(MAX_COMPACTION_SUMMARY_CHARS)}`;
const oversizedRequiredTail = [
"## Decisions",
"Keep current flow.",
"## Open TODOs",
"None.",
"## Constraints/Rules",
"Preserve exact context.",
"## Pending user asks",
latestAsk,
"## Exact identifiers",
identifier,
].join("\n");
mockSummarizeInStages.mockResolvedValue(summaryResult(oversizedRequiredTail));
const sessionManager = stubSessionManager();
setCompactionSafeguardRuntime(sessionManager, {
model: createAnthropicModelFixture(),
recentTurnsPreserve: 0,
qualityGuardEnabled: true,
qualityGuardMaxRetries: 0,
});
const event = createCompactionEvent({
messageText: `${latestAsk} ${identifier}`,
tokensBefore: 1_500,
});
(
event.preparation as { settings?: { reserveTokens: number }; isSplitTurn?: boolean }
).settings = { reserveTokens: 4_000 };
(event.preparation as { isSplitTurn?: boolean }).isSplitTurn = false;
const { result } = await runCompactionScenario({ sessionManager, event, apiKey: "test-key" });
expect(result).toEqual({ cancel: true });
expect(mockSummarizeInStages).toHaveBeenCalledTimes(1);
expect(consumeCompactionSafeguardCancelReason(sessionManager)).toBe(
"Compaction safeguard required facts exceed the finalized summary budget.",
);
});
it("reserves split-turn ask evidence and identifiers before optional split context", async () => {
mockSummarizeInStages.mockReset();
const latestAsk = "preserve the deployment status";
const identifier = "/tmp/split-turn-retention.log";
const historySummary = [
"## Decisions",
"x".repeat(MAX_COMPACTION_SUMMARY_CHARS),
"## Open TODOs",
"None.",
"## Constraints/Rules",
"Preserve exact context.",
"## Pending user asks",
"Continue the active work.",
"## Exact identifiers",
"None.",
].join("\n");
const splitSummary = `${latestAsk} ${identifier} ${"z".repeat(MAX_COMPACTION_SUMMARY_CHARS)}`;
mockSummarizeInStages
.mockResolvedValueOnce(summaryResult(historySummary))
.mockResolvedValueOnce(summaryResult(splitSummary));
const sessionManager = stubSessionManager();
setCompactionSafeguardRuntime(sessionManager, {
model: createAnthropicModelFixture(),
recentTurnsPreserve: 0,
qualityGuardEnabled: true,
qualityGuardMaxRetries: 1,
});
const event = {
preparation: {
messagesToSummarize: [
{ role: "user", content: "summarize earlier work", timestamp: 1 },
] as AgentMessage[],
turnPrefixMessages: [
{ role: "user", content: `${latestAsk} ${identifier}`, timestamp: 2 },
] as AgentMessage[],
firstKeptEntryId: "entry-1",
tokensBefore: 1_500,
fileOps: { read: [], edited: [], written: [] },
settings: { reserveTokens: 4_000 },
isSplitTurn: true,
},
customInstructions: "",
signal: new AbortController().signal,
};
const { result } = await runCompactionScenario({ sessionManager, event, apiKey: "test-key" });
const summary = expectCompactionResult(result).summary;
expect(summary.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS);
expect(summary).toContain(identifier);
expect(auditSummaryQuality({ summary, identifiers: [identifier], latestAsk }).ok).toBe(true);
expect(mockSummarizeInStages).toHaveBeenCalledTimes(2);
const reason = consumeCompactionSafeguardCancelReason(sessionManager);
expect(reason).toContain("finalized summary failed quality checks");
expect(reason).not.toContain(identifier);
expect(reason).not.toContain(latestAsk);
});
it("returns the first finalized retry that passes the source audit", async () => {
+99 -9
View File
@@ -62,6 +62,7 @@ import {
auditSummaryQuality,
buildCompactionStructureInstructions,
buildStructuredFallbackSummary,
createSummaryQualityRetentionPlan,
extractOpaqueIdentifiers,
wrapUntrustedInstructionBlock,
} from "./compaction-safeguard-quality.js";
@@ -90,6 +91,8 @@ const DEFAULT_QUALITY_GUARD_MAX_RETRIES = 1;
const MAX_RECENT_TURNS_PRESERVE = 12;
const MAX_QUALITY_GUARD_MAX_RETRIES = 3;
const MAX_RECENT_TURN_TEXT_CHARS = 600;
const MAX_REQUIRED_ASK_CONTEXT_CHARS = 2_000;
const REQUIRED_ASK_CONTEXT_TRUNCATED_MARKER = "\n[... split-turn ask context truncated ...]\n";
const TOOL_CALL_BLOCK_TYPES = new Set(["toolCall", "toolUse", "functionCall"]);
const PREVIOUS_SUMMARY_REDISTILL_PREFIX =
"Previous compaction summary to re-distill with the current conversation. " +
@@ -334,8 +337,17 @@ type CompactionSuffix = {
contextRanges: Array<{ start: number; end: number; segmentStarts: number[] }>;
};
type SummaryQualityRetention = {
auditSummary: string;
identifiers: string[];
latestAsk: string | null;
requiredAskContext: string;
identifierPolicy: "strict" | "off" | "custom";
};
function assembleSuffix(parts: {
splitTurnSection?: ContextSection;
generatedSplitTurnSection?: string;
preservedTurnsSection?: ContextSection;
toolFailureSection?: string;
fileOpsSummary?: string;
@@ -605,6 +617,7 @@ function budgetCompactionSummary(
summaryBody: string,
suffixInput: string | CompactionSuffix,
maxChars: number,
qualityRetention?: SummaryQualityRetention,
) {
const suffix = normalizeCompactionSuffix(suffixInput);
const joined = `${summaryBody}${suffix.text}`;
@@ -615,13 +628,22 @@ function budgetCompactionSummary(
bodyBudget: maxChars,
bodyTrimmed: false,
suffixTrimmed: false,
qualityRetentionInfeasible: false,
};
}
const bodyFloor = Math.min(summaryBody.length, Math.max(1, Math.ceil(maxChars / 2)));
const retentionPlan = qualityRetention
? createSummaryQualityRetentionPlan(summaryBody, SUMMARY_TRUNCATED_MARKER, qualityRetention)
: null;
const bodyCapacity = retentionPlan ? maxChars : summaryBody.length;
const bodyFloor = Math.min(
bodyCapacity,
maxChars,
Math.max(1, Math.ceil(maxChars / 2), retentionPlan?.minimumChars ?? 0),
);
const suffixReservation = Math.min(suffix.text.length, maxChars);
const bodySlot = Math.min(summaryBody.length, Math.max(bodyFloor, maxChars - suffixReservation));
const cappedBody = capCompactionSummary(summaryBody, bodySlot);
const bodySlot = Math.min(bodyCapacity, Math.max(bodyFloor, maxChars - suffixReservation));
const cappedBody = retentionPlan?.render(bodySlot) ?? capCompactionSummary(summaryBody, bodySlot);
const suffixBudget = Math.max(0, maxChars - cappedBody.length);
const cappedSuffix = capCompactionSuffix(suffix, suffixBudget);
return {
@@ -630,6 +652,7 @@ function budgetCompactionSummary(
bodyBudget: bodySlot,
bodyTrimmed: cappedBody.length < summaryBody.length,
suffixTrimmed: cappedSuffix.length < suffix.text.length,
qualityRetentionInfeasible: retentionPlan !== null && retentionPlan.minimumChars > maxChars,
};
}
@@ -904,6 +927,33 @@ function formatGeneratedSplitTurnSection(summary: string, onTruncated?: () => vo
return `${heading}${cappedSummary}`;
}
function formatRequiredAskContext(summary: string): string {
const originalRequestHeading = "## Original Request";
const earlyProgressHeading = "## Early Progress";
const originalRequestStart = summary.indexOf(originalRequestHeading);
const originalRequestEnd =
originalRequestStart >= 0
? summary.indexOf(earlyProgressHeading, originalRequestStart + originalRequestHeading.length)
: -1;
const source =
originalRequestStart >= 0
? summary
.slice(
originalRequestStart + originalRequestHeading.length,
originalRequestEnd >= 0 ? originalRequestEnd : undefined,
)
.trim()
: summary.trim();
if (source.length <= MAX_REQUIRED_ASK_CONTEXT_CHARS) {
return source;
}
const contentBudget =
MAX_REQUIRED_ASK_CONTEXT_CHARS - REQUIRED_ASK_CONTEXT_TRUNCATED_MARKER.length;
const headBudget = Math.floor(contentBudget / 2);
const tailBudget = contentBudget - headBudget;
return `${truncateUtf16Safe(source, headBudget)}${REQUIRED_ASK_CONTEXT_TRUNCATED_MARKER}${sliceUtf16Safe(source, -tailBudget)}`;
}
function extractLatestUserAsk(messages: AgentMessage[]): string | null {
for (const message of messages.toReversed()) {
if (message.role !== "user") {
@@ -1061,6 +1111,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
identifierInstructions: runtime?.identifierInstructions,
};
const identifierPolicy = runtime?.identifierPolicy ?? "strict";
const qualityGuardEnabled = runtime?.qualityGuardEnabled ?? false;
const providerId = runtime?.provider;
const turnPrefixMessages = baseTurnPrefixMessages;
const recentTurnsPreserve = resolveRecentTurnsPreserve(runtime?.recentTurnsPreserve);
@@ -1071,23 +1122,35 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
let workspaceContextPromise: Promise<string> | undefined;
const finalizeSummaryText = async (
body: string,
sections: { splitTurnSection?: ContextSection; preservedTurnsSection?: ContextSection },
sections: {
splitTurnSection?: ContextSection;
generatedSplitTurnSection?: string;
preservedTurnsSection?: ContextSection;
},
producerLosses: ReadonlySet<CompactionLoss> = new Set(),
qualityRetention?: SummaryQualityRetention,
) => {
workspaceContextPromise ??= readWorkspaceContextForSummary(
runtime?.postCompactionSections,
runtime?.workspaceDir,
);
const suffix = assembleSuffix({
...sections,
splitTurnSection: sections.splitTurnSection,
generatedSplitTurnSection: sections.generatedSplitTurnSection,
preservedTurnsSection: sections.preservedTurnsSection,
toolFailureSection,
fileOpsSummary,
workspaceContext: await workspaceContextPromise,
});
const finalized = budgetCompactionSummary(body, suffix, MAX_COMPACTION_SUMMARY_CHARS);
const finalized = budgetCompactionSummary(
body,
suffix,
MAX_COMPACTION_SUMMARY_CHARS,
qualityRetention,
);
const losses = new Set(producerLosses);
for (const section of Object.values(sections)) {
if (section?.truncatedLoss) {
if (typeof section !== "string" && section?.truncatedLoss) {
losses.add(section.truncatedLoss);
}
}
@@ -1205,7 +1268,6 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
thinkingLevel,
streamFn,
};
const qualityGuardEnabled = runtime?.qualityGuardEnabled ?? false;
const qualityGuardMaxRetries = resolveQualityGuardMaxRetries(runtime?.qualityGuardMaxRetries);
const maxHistoryShare = runtime?.maxHistoryShare ?? 0.5;
@@ -1309,6 +1371,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
for (let attempt = 0; attempt < totalAttempts; attempt += 1) {
let splitTurnSectionLocal = "";
let splitTurnAskContextLocal = "";
let historySummary = "";
const producerLosses = new Set<CompactionLoss>();
try {
@@ -1334,6 +1397,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
splitTurnSectionLocal = formatGeneratedSplitTurnSection(prefixSummary, () => {
producerLosses.add("split-turn-tail");
});
splitTurnAskContextLocal = formatRequiredAskContext(prefixSummary);
}
} catch (attemptError) {
if (signal?.aborted) {
@@ -1352,16 +1416,31 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
}
throw attemptError;
}
const structuralSummary = appendSummarySection(
const unbudgetedSummary = appendSummarySection(
historySummary,
splitTurnSectionLocal ? `\n\n${splitTurnSectionLocal}` : "",
);
const structuralSummary = qualityGuardEnabled ? historySummary : unbudgetedSummary;
const finalized = await finalizeSummaryText(
structuralSummary,
{
generatedSplitTurnSection:
qualityGuardEnabled && splitTurnSectionLocal
? `\n\n${splitTurnSectionLocal}`
: undefined,
preservedTurnsSection: preservedTurnsSectionLocal,
},
producerLosses,
qualityGuardEnabled
? {
auditSummary: unbudgetedSummary,
identifiers,
latestAsk: latestUserAsk,
requiredAskContext:
splitTurnAskContextLocal || formatRequiredAskContext(latestUserAsk ?? ""),
identifierPolicy,
}
: undefined,
);
const canRegenerate =
@@ -1370,6 +1449,17 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
if (!qualityGuardEnabled) {
return compactionResult(finalized.summary);
}
if (finalized.qualityRetentionInfeasible) {
log.warn(
"Compaction safeguard: required quality facts exceed finalized artifact budget; " +
`requiredChars>${MAX_COMPACTION_SUMMARY_CHARS} identifierCount=${identifiers.length}`,
);
setCompactionSafeguardCancelReason(
ctx.sessionManager,
"Compaction safeguard required facts exceed the finalized summary budget.",
);
return { cancel: true };
}
const quality = auditSummaryQuality({
summary: finalized.summary,
structuralSummary: finalized.structuralSummary,