mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-22 22:52:03 +00:00
refactor: split compaction safeguard quality helpers
This commit is contained in:
247
src/agents/pi-extensions/compaction-safeguard-quality.ts
Normal file
247
src/agents/pi-extensions/compaction-safeguard-quality.ts
Normal 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 };
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user