UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

154 lines (153 loc) • 6.03 kB
import { r as resolveAgentConfig } from "./agent-scope-config-BU4qc9bA.mjs"; import "./agent-scope-runtime-kyXy8x5x.mjs"; //#region extensions/memory-core/src/memory-budget.ts const PROMOTION_SECTION_HEADING_RE = /^## Promoted From Short-Term Memory \(([^)]+)\)\s*$/; const PROMOTION_SUBSECTION_HEADING_RE = /^### (?:Global|Project: .+?)\s*$/; const PROMOTION_ENTRY_MARKER_RE = /^<!--\s*openclaw-memory-promotion:.*-->\s*$/i; const ATX_HEADING_RE = /^ {0,3}#{1,6}(?:[ \t]|$)/; const SETEXT_HEADING_UNDERLINE_RE = /^ {0,3}(?:=+|-+)[ \t]*$/; const DEFAULT_MEMORY_FILE_MAX_CHARS = 1e4; /** * Keep promotion output within every consuming agent's per-file bootstrap * budget. An unconfigured bootstrap limit stays above the promotion writer's * own ceiling, so only explicit lower limits need to reduce the budget here. */ function resolveMemoryPromotionFileMaxChars(params) { const defaultBootstrapLimit = params.cfg?.agents?.defaults?.bootstrapMaxChars; const agentIds = params.agentIds.length > 0 ? [...new Set(params.agentIds)] : [void 0]; let limit = DEFAULT_MEMORY_FILE_MAX_CHARS; for (const agentId of agentIds) { const configuredLimit = agentId ? resolveAgentConfig(params.cfg ?? {}, agentId)?.bootstrapMaxChars ?? defaultBootstrapLimit : defaultBootstrapLimit; if (typeof configuredLimit === "number" && Number.isFinite(configuredLimit) && configuredLimit > 0) limit = Math.min(limit, Math.floor(configuredLimit)); } return limit; } const WRITE_OVERHEAD_RESERVE = 21; function isGeneratedPromotionBlock(lines) { let sawEntry = false; let index = 1; while (index < lines.length) { const line = lines[index] ?? ""; if (line.trim().length === 0) { index += 1; continue; } if (PROMOTION_SUBSECTION_HEADING_RE.test(line)) { index += 1; while (index < lines.length && (lines[index] ?? "").trim().length === 0) index += 1; } if (!PROMOTION_ENTRY_MARKER_RE.test(lines[index] ?? "")) return false; if (!(lines[index + 1] ?? "").startsWith("- ")) return false; sawEntry = true; index += 2; } return sawEntry; } function startsGeneratedPromotionSubsection(lines, index) { if (!PROMOTION_SUBSECTION_HEADING_RE.test(lines[index] ?? "")) return false; for (let next = index + 1; next < lines.length; next += 1) { const line = lines[next] ?? ""; if (line.trim().length === 0) continue; return PROMOTION_ENTRY_MARKER_RE.test(line); } return false; } function takeSetextHeadingLines(lines) { let start = lines.length; while (start > 1 && lines[start - 1]?.trim().length !== 0) start -= 1; if (start === lines.length) return; const headingLines = lines.slice(start); if (headingLines.some((line) => PROMOTION_ENTRY_MARKER_RE.test(line))) return; lines.splice(start); return headingLines; } function parseMemoryBlocks(content) { if (content.length === 0) return []; const lines = content.split(/\r?\n/); const blocks = []; let currentLines = []; let currentDate; const flush = () => { if (currentLines.length === 0) return; const text = currentLines.join("\n"); if (currentDate && isGeneratedPromotionBlock(currentLines)) blocks.push({ kind: "promotion", date: currentDate, text, entryCount: currentLines.filter((line) => PROMOTION_ENTRY_MARKER_RE.test(line)).length }); else blocks.push({ kind: "preserved", text }); currentLines = []; currentDate = void 0; }; for (const [index, line] of lines.entries()) { if (currentDate && SETEXT_HEADING_UNDERLINE_RE.test(line)) { const headingLines = takeSetextHeadingLines(currentLines); if (headingLines) { flush(); currentLines = [...headingLines, line]; continue; } } const continuesPromotionBody = currentDate && startsGeneratedPromotionSubsection(lines, index); if (ATX_HEADING_RE.test(line) && !continuesPromotionBody) { flush(); currentDate = PROMOTION_SECTION_HEADING_RE.exec(line)?.[1]; currentLines = [line]; } else currentLines.push(line); } flush(); return blocks; } /** Drop oldest fully generated sections within the loss bound; the caller owns the final fit check. */ function compactMemoryForBudget(params) { const { existingMemory, newSection, budgetChars } = params; if (budgetChars <= 0) return { compacted: existingMemory, droppedDates: [] }; const effectiveBudget = Math.max(0, budgetChars - WRITE_OVERHEAD_RESERVE); if (existingMemory.length + newSection.length <= effectiveBudget) return { compacted: existingMemory, droppedDates: [] }; const blocks = parseMemoryBlocks(existingMemory); const promotionEntries = blocks.flatMap((block, index) => block.kind === "promotion" ? [{ index, date: block.date, length: block.text.length, entryCount: block.entryCount }] : []).toSorted((a, b) => a.date.localeCompare(b.date)); if (promotionEntries.length === 0) return { compacted: existingMemory, droppedDates: [] }; const droppedIndices = /* @__PURE__ */ new Set(); const droppedDates = []; const totalEntryCount = promotionEntries.reduce((total, entry) => total + entry.entryCount, 0); const maxLossFraction = Math.max(0, Math.min(1, params.maxPriorEntryLossFraction ?? 1)); let droppedEntryCount = 0; let projectedExistingSize = existingMemory.length; const blockSeparatorCost = blocks.length > 1 ? 1 : 0; for (const entry of promotionEntries) { if (projectedExistingSize + newSection.length <= effectiveBudget) break; if (totalEntryCount > 0 && (droppedEntryCount + entry.entryCount) / totalEntryCount > maxLossFraction) break; droppedIndices.add(entry.index); droppedDates.push(entry.date); droppedEntryCount += entry.entryCount; projectedExistingSize = Math.max(0, projectedExistingSize - entry.length - blockSeparatorCost); } if (droppedIndices.size === 0) return { compacted: existingMemory, droppedDates: [] }; return { compacted: blocks.filter((_, index) => !droppedIndices.has(index)).map((block) => block.text).join("\n"), droppedDates }; } //#endregion export { compactMemoryForBudget as n, resolveMemoryPromotionFileMaxChars as r, DEFAULT_MEMORY_FILE_MAX_CHARS as t };