refactor: split compaction safeguard quality helpers

This commit is contained in:
Peter Steinberger
2026-03-27 01:55:58 +00:00
parent 40bd36e35d
commit 60a8dd95de
2 changed files with 255 additions and 248 deletions

View File

@@ -0,0 +1,247 @@
import {
extractKeywords,
isQueryStopWordToken,
} from "../../plugins/memory-host/query-expansion.js";
import type { CompactionSummarizationInstructions } from "../compaction.js";
import { wrapUntrustedPromptDataBlock } from "../sanitize-for-prompt.js";
const MAX_EXTRACTED_IDENTIFIERS = 12;
const MAX_UNTRUSTED_INSTRUCTION_CHARS = 4000;
const MAX_ASK_OVERLAP_TOKENS = 12;
const MIN_ASK_OVERLAP_TOKENS_FOR_DOUBLE_MATCH = 3;
const REQUIRED_SUMMARY_SECTIONS = [
"## Decisions",
"## Open TODOs",
"## Constraints/Rules",
"## Pending user asks",
"## Exact identifiers",
] as const;
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 =
"For ## Exact identifiers, include identifiers only when needed for continuity; do not enforce literal-preservation rules.";
export function wrapUntrustedInstructionBlock(label: string, text: string): string {
return wrapUntrustedPromptDataBlock({
label,
text,
maxChars: MAX_UNTRUSTED_INSTRUCTION_CHARS,
});
}
function resolveExactIdentifierSectionInstruction(
summarizationInstructions?: CompactionSummarizationInstructions,
): string {
const policy = summarizationInstructions?.identifierPolicy ?? "strict";
if (policy === "off") {
return POLICY_OFF_EXACT_IDENTIFIERS_INSTRUCTION;
}
if (policy === "custom") {
const custom = summarizationInstructions?.identifierInstructions?.trim();
if (custom) {
const customBlock = wrapUntrustedInstructionBlock(
"For ## Exact identifiers, apply this operator-defined policy text",
custom,
);
if (customBlock) {
return customBlock;
}
}
}
return STRICT_EXACT_IDENTIFIERS_INSTRUCTION;
}
export function buildCompactionStructureInstructions(
customInstructions?: string,
summarizationInstructions?: CompactionSummarizationInstructions,
): string {
const identifierSectionInstruction =
resolveExactIdentifierSectionInstruction(summarizationInstructions);
const sectionsTemplate = [
"Produce a compact, factual summary with these exact section headings:",
...REQUIRED_SUMMARY_SECTIONS,
identifierSectionInstruction,
"Do not omit unresolved asks from the user.",
].join("\n");
const custom = customInstructions?.trim();
if (!custom) {
return sectionsTemplate;
}
const customBlock = wrapUntrustedInstructionBlock("Additional context from /compact", custom);
if (!customBlock) {
return sectionsTemplate;
}
return `${sectionsTemplate}\n\n${customBlock}`;
}
function normalizedSummaryLines(summary: string): string[] {
return summary
.split(/\r?\n/u)
.map((line) => line.trim())
.filter((line) => line.length > 0);
}
function hasRequiredSummarySections(summary: string): boolean {
const lines = normalizedSummaryLines(summary);
let cursor = 0;
for (const heading of REQUIRED_SUMMARY_SECTIONS) {
const index = lines.findIndex((line, lineIndex) => lineIndex >= cursor && line === heading);
if (index < 0) {
return false;
}
cursor = index + 1;
}
return true;
}
export function buildStructuredFallbackSummary(
previousSummary: string | undefined,
_summarizationInstructions?: CompactionSummarizationInstructions,
): string {
const trimmedPreviousSummary = previousSummary?.trim() ?? "";
if (trimmedPreviousSummary && hasRequiredSummarySections(trimmedPreviousSummary)) {
return trimmedPreviousSummary;
}
const exactIdentifiersSummary = "None captured.";
return [
"## Decisions",
trimmedPreviousSummary || "No prior history.",
"",
"## Open TODOs",
"None.",
"",
"## Constraints/Rules",
"None.",
"",
"## Pending user asks",
"None.",
"",
"## Exact identifiers",
exactIdentifiersSummary,
].join("\n");
}
export function appendSummarySection(summary: string, section: string): string {
if (!section) {
return summary;
}
if (!summary.trim()) {
return section.trimStart();
}
return `${summary}${section}`;
}
function sanitizeExtractedIdentifier(value: string): string {
return value
.trim()
.replace(/^[("'`[{<]+/, "")
.replace(/[)\]"'`,;:.!?<>]+$/, "");
}
function isPureHexIdentifier(value: string): boolean {
return /^[A-Fa-f0-9]{8,}$/.test(value);
}
function normalizeOpaqueIdentifier(value: string): string {
return isPureHexIdentifier(value) ? value.toUpperCase() : value;
}
function summaryIncludesIdentifier(summary: string, identifier: string): boolean {
if (isPureHexIdentifier(identifier)) {
return summary.toUpperCase().includes(identifier.toUpperCase());
}
return summary.includes(identifier);
}
export function extractOpaqueIdentifiers(text: string): string[] {
const matches =
text.match(
/([A-Fa-f0-9]{8,}|https?:\/\/\S+|\/[\w.-]{2,}(?:\/[\w.-]+)+|[A-Za-z]:\\[\w\\.-]+|[A-Za-z0-9._-]+\.[A-Za-z0-9._/-]+:\d{1,5}|\b\d{6,}\b)/g,
) ?? [];
return Array.from(
new Set(
matches
.map((value) => sanitizeExtractedIdentifier(value))
.map((value) => normalizeOpaqueIdentifier(value))
.filter((value) => value.length >= 4),
),
).slice(0, MAX_EXTRACTED_IDENTIFIERS);
}
function tokenizeAskOverlapText(text: string): string[] {
const normalized = text.toLocaleLowerCase().normalize("NFKC").trim();
if (!normalized) {
return [];
}
const keywords = extractKeywords(normalized);
if (keywords.length > 0) {
return keywords;
}
return normalized
.split(/[^\p{L}\p{N}]+/u)
.map((token) => token.trim())
.filter((token) => token.length > 0);
}
function hasAskOverlap(summary: string, latestAsk: string | null): boolean {
if (!latestAsk) {
return true;
}
const askTokens = Array.from(new Set(tokenizeAskOverlapText(latestAsk))).slice(
0,
MAX_ASK_OVERLAP_TOKENS,
);
if (askTokens.length === 0) {
return true;
}
const meaningfulAskTokens = askTokens.filter((token) => {
if (token.length <= 1) {
return false;
}
if (isQueryStopWordToken(token)) {
return false;
}
return true;
});
const tokensToCheck = meaningfulAskTokens.length > 0 ? meaningfulAskTokens : askTokens;
if (tokensToCheck.length === 0) {
return true;
}
const summaryTokens = new Set(tokenizeAskOverlapText(summary));
let overlapCount = 0;
for (const token of tokensToCheck) {
if (summaryTokens.has(token)) {
overlapCount += 1;
}
}
const requiredMatches = tokensToCheck.length >= MIN_ASK_OVERLAP_TOKENS_FOR_DOUBLE_MATCH ? 2 : 1;
return overlapCount >= requiredMatches;
}
export function auditSummaryQuality(params: {
summary: string;
identifiers: string[];
latestAsk: string | null;
identifierPolicy?: CompactionSummarizationInstructions["identifierPolicy"];
}): { ok: boolean; reasons: string[] } {
const reasons: string[] = [];
const lines = new Set(normalizedSummaryLines(params.summary));
for (const section of REQUIRED_SUMMARY_SECTIONS) {
if (!lines.has(section)) {
reasons.push(`missing_section:${section}`);
}
}
const enforceIdentifiers = (params.identifierPolicy ?? "strict") === "strict";
if (enforceIdentifiers) {
const missingIdentifiers = params.identifiers.filter(
(identifier) => !summaryIncludesIdentifier(params.summary, identifier),
);
if (missingIdentifiers.length > 0) {
reasons.push(`missing_identifiers:${missingIdentifiers.slice(0, 3).join(",")}`);
}
}
if (!hasAskOverlap(params.summary, params.latestAsk)) {
reasons.push("latest_user_ask_not_reflected");
}
return { ok: reasons.length === 0, reasons };
}

View File

@@ -5,17 +5,12 @@ import type { ExtensionAPI, FileOperations } from "@mariozechner/pi-coding-agent
import { extractSections } from "../../auto-reply/reply/post-compaction-context.js";
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
extractKeywords,
isQueryStopWordToken,
} from "../../plugins/memory-host/query-expansion.js";
import {
hasMeaningfulConversationContent,
isRealConversationMessage,
} from "../compaction-real-conversation.js";
import {
BASE_CHUNK_RATIO,
type CompactionSummarizationInstructions,
MIN_CHUNK_RATIO,
SAFETY_MARGIN,
SUMMARIZATION_OVERHEAD_TOKENS,
@@ -27,13 +22,20 @@ import {
summarizeInStages,
} from "../compaction.js";
import { collectTextContentBlocks } from "../content-blocks.js";
import { wrapUntrustedPromptDataBlock } from "../sanitize-for-prompt.js";
import { repairToolUseResultPairing } from "../session-transcript-repair.js";
import { extractToolCallsFromAssistant, extractToolResultId } from "../tool-call-id.js";
import {
composeSplitTurnInstructions,
resolveCompactionInstructions,
} from "./compaction-instructions.js";
import {
appendSummarySection,
auditSummaryQuality,
buildCompactionStructureInstructions,
buildStructuredFallbackSummary,
extractOpaqueIdentifiers,
wrapUntrustedInstructionBlock,
} from "./compaction-safeguard-quality.js";
import {
getCompactionSafeguardRuntime,
setCompactionSafeguardCancelReason,
@@ -57,21 +59,6 @@ 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_EXTRACTED_IDENTIFIERS = 12;
const MAX_UNTRUSTED_INSTRUCTION_CHARS = 4000;
const MAX_ASK_OVERLAP_TOKENS = 12;
const MIN_ASK_OVERLAP_TOKENS_FOR_DOUBLE_MATCH = 3;
const REQUIRED_SUMMARY_SECTIONS = [
"## Decisions",
"## Open TODOs",
"## Constraints/Rules",
"## Pending user asks",
"## Exact identifiers",
] as const;
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 =
"For ## Exact identifiers, include identifiers only when needed for continuity; do not enforce literal-preservation rules.";
const compactionSafeguardDeps = {
summarizeInStages,
};
@@ -488,155 +475,6 @@ function formatPreservedTurnsSection(messages: AgentMessage[]): string {
return `\n\n## Recent turns preserved verbatim\n${lines.join("\n")}`;
}
function wrapUntrustedInstructionBlock(label: string, text: string): string {
return wrapUntrustedPromptDataBlock({
label,
text,
maxChars: MAX_UNTRUSTED_INSTRUCTION_CHARS,
});
}
function resolveExactIdentifierSectionInstruction(
summarizationInstructions?: CompactionSummarizationInstructions,
): string {
const policy = summarizationInstructions?.identifierPolicy ?? "strict";
if (policy === "off") {
return POLICY_OFF_EXACT_IDENTIFIERS_INSTRUCTION;
}
if (policy === "custom") {
const custom = summarizationInstructions?.identifierInstructions?.trim();
if (custom) {
const customBlock = wrapUntrustedInstructionBlock(
"For ## Exact identifiers, apply this operator-defined policy text",
custom,
);
if (customBlock) {
return customBlock;
}
}
}
return STRICT_EXACT_IDENTIFIERS_INSTRUCTION;
}
function buildCompactionStructureInstructions(
customInstructions?: string,
summarizationInstructions?: CompactionSummarizationInstructions,
): string {
const identifierSectionInstruction =
resolveExactIdentifierSectionInstruction(summarizationInstructions);
const sectionsTemplate = [
"Produce a compact, factual summary with these exact section headings:",
...REQUIRED_SUMMARY_SECTIONS,
identifierSectionInstruction,
"Do not omit unresolved asks from the user.",
].join("\n");
const custom = customInstructions?.trim();
if (!custom) {
return sectionsTemplate;
}
const customBlock = wrapUntrustedInstructionBlock("Additional context from /compact", custom);
if (!customBlock) {
return sectionsTemplate;
}
// summarizeInStages already wraps custom instructions once with "Additional focus:".
// Keep this helper label-free to avoid nested/duplicated headers.
return `${sectionsTemplate}\n\n${customBlock}`;
}
function normalizedSummaryLines(summary: string): string[] {
return summary
.split(/\r?\n/u)
.map((line) => line.trim())
.filter((line) => line.length > 0);
}
function hasRequiredSummarySections(summary: string): boolean {
const lines = normalizedSummaryLines(summary);
let cursor = 0;
for (const heading of REQUIRED_SUMMARY_SECTIONS) {
const index = lines.findIndex((line, lineIndex) => lineIndex >= cursor && line === heading);
if (index < 0) {
return false;
}
cursor = index + 1;
}
return true;
}
function buildStructuredFallbackSummary(
previousSummary: string | undefined,
_summarizationInstructions?: CompactionSummarizationInstructions,
): string {
const trimmedPreviousSummary = previousSummary?.trim() ?? "";
if (trimmedPreviousSummary && hasRequiredSummarySections(trimmedPreviousSummary)) {
return trimmedPreviousSummary;
}
const exactIdentifiersSummary = "None captured.";
return [
"## Decisions",
trimmedPreviousSummary || "No prior history.",
"",
"## Open TODOs",
"None.",
"",
"## Constraints/Rules",
"None.",
"",
"## Pending user asks",
"None.",
"",
"## Exact identifiers",
exactIdentifiersSummary,
].join("\n");
}
function appendSummarySection(summary: string, section: string): string {
if (!section) {
return summary;
}
if (!summary.trim()) {
return section.trimStart();
}
return `${summary}${section}`;
}
function sanitizeExtractedIdentifier(value: string): string {
return value
.trim()
.replace(/^[("'`[{<]+/, "")
.replace(/[)\]"'`,;:.!?<>]+$/, "");
}
function isPureHexIdentifier(value: string): boolean {
return /^[A-Fa-f0-9]{8,}$/.test(value);
}
function normalizeOpaqueIdentifier(value: string): string {
return isPureHexIdentifier(value) ? value.toUpperCase() : value;
}
function summaryIncludesIdentifier(summary: string, identifier: string): boolean {
if (isPureHexIdentifier(identifier)) {
return summary.toUpperCase().includes(identifier.toUpperCase());
}
return summary.includes(identifier);
}
function extractOpaqueIdentifiers(text: string): string[] {
const matches =
text.match(
/([A-Fa-f0-9]{8,}|https?:\/\/\S+|\/[\w.-]{2,}(?:\/[\w.-]+)+|[A-Za-z]:\\[\w\\.-]+|[A-Za-z0-9._-]+\.[A-Za-z0-9._/-]+:\d{1,5}|\b\d{6,}\b)/g,
) ?? [];
return Array.from(
new Set(
matches
.map((value) => sanitizeExtractedIdentifier(value))
.map((value) => normalizeOpaqueIdentifier(value))
.filter((value) => value.length >= 4),
),
).slice(0, MAX_EXTRACTED_IDENTIFIERS);
}
function extractLatestUserAsk(messages: AgentMessage[]): string | null {
for (let i = messages.length - 1; i >= 0; i -= 1) {
const message = messages[i];
@@ -651,84 +489,6 @@ function extractLatestUserAsk(messages: AgentMessage[]): string | null {
return null;
}
function tokenizeAskOverlapText(text: string): string[] {
const normalized = text.toLocaleLowerCase().normalize("NFKC").trim();
if (!normalized) {
return [];
}
const keywords = extractKeywords(normalized);
if (keywords.length > 0) {
return keywords;
}
return normalized
.split(/[^\p{L}\p{N}]+/u)
.map((token) => token.trim())
.filter((token) => token.length > 0);
}
function hasAskOverlap(summary: string, latestAsk: string | null): boolean {
if (!latestAsk) {
return true;
}
const askTokens = Array.from(new Set(tokenizeAskOverlapText(latestAsk))).slice(
0,
MAX_ASK_OVERLAP_TOKENS,
);
if (askTokens.length === 0) {
return true;
}
const meaningfulAskTokens = askTokens.filter((token) => {
if (token.length <= 1) {
return false;
}
if (isQueryStopWordToken(token)) {
return false;
}
return true;
});
const tokensToCheck = meaningfulAskTokens.length > 0 ? meaningfulAskTokens : askTokens;
if (tokensToCheck.length === 0) {
return true;
}
const summaryTokens = new Set(tokenizeAskOverlapText(summary));
let overlapCount = 0;
for (const token of tokensToCheck) {
if (summaryTokens.has(token)) {
overlapCount += 1;
}
}
const requiredMatches = tokensToCheck.length >= MIN_ASK_OVERLAP_TOKENS_FOR_DOUBLE_MATCH ? 2 : 1;
return overlapCount >= requiredMatches;
}
function auditSummaryQuality(params: {
summary: string;
identifiers: string[];
latestAsk: string | null;
identifierPolicy?: CompactionSummarizationInstructions["identifierPolicy"];
}): { ok: boolean; reasons: string[] } {
const reasons: string[] = [];
const lines = new Set(normalizedSummaryLines(params.summary));
for (const section of REQUIRED_SUMMARY_SECTIONS) {
if (!lines.has(section)) {
reasons.push(`missing_section:${section}`);
}
}
const enforceIdentifiers = (params.identifierPolicy ?? "strict") === "strict";
if (enforceIdentifiers) {
const missingIdentifiers = params.identifiers.filter(
(id) => !summaryIncludesIdentifier(params.summary, id),
);
if (missingIdentifiers.length > 0) {
reasons.push(`missing_identifiers:${missingIdentifiers.slice(0, 3).join(",")}`);
}
}
if (!hasAskOverlap(params.summary, params.latestAsk)) {
reasons.push("latest_user_ask_not_reflected");
}
return { ok: reasons.length === 0, reasons };
}
/**
* Read and format critical workspace context for compaction summary.
* Extracts "Session Startup" and "Red Lines" from AGENTS.md.