import fs from "node:fs"; import path from "node:path"; import { resolveAgentContextLimits } from "../../agents/agent-scope.js"; import { resolveCronStyleNow } from "../../agents/current-time.js"; import { resolveUserTimezone } from "../../agents/date-time.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { openBoundaryFile } from "../../infra/boundary-file-read.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
// Compare configured section names as a case-insensitive set so deployments can // pin the documented defaults in any order without changing fallback semantics. function matchesSectionSet(sectionNames: string[], expectedSections: string[]): boolean { if (sectionNames.length !== expectedSections.length) { returnfalse;
}
const counts = new Map<string, number>(); for (const name of expectedSections) { const normalized = normalizeLowercaseStringOrEmpty(name);
counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
}
for (const name of sectionNames) { const normalized = normalizeLowercaseStringOrEmpty(name); const count = counts.get(normalized); if (!count) { returnfalse;
} if (count === 1) {
counts.delete(normalized);
} else {
counts.set(normalized, count - 1);
}
}
// Fall back to legacy section names ("Every Session" / "Safety") when using // defaults and the current headings aren't found — preserves compatibility // with older AGENTS.md templates. The fallback also applies when the user // explicitly configures the default pair, so that pinning the documented // defaults never silently changes behavior vs. leaving the field unset. const isDefaultSections =
!Array.isArray(configuredSections) ||
matchesSectionSet(configuredSections, DEFAULT_POST_COMPACTION_SECTIONS); if (sections.length === 0 && isDefaultSections) {
sections = extractSections(content, LEGACY_POST_COMPACTION_SECTIONS, foundSectionNames);
}
if (sections.length === 0) { returnnull;
}
// Only reference section names that were actually found and injected. const displayNames = foundSectionNames.length > 0 ? foundSectionNames : sectionNames;
const resolvedNowMs = effectiveNowMs ?? Date.now(); const timezone = resolveUserTimezone(cfg?.agents?.defaults?.userTimezone); const dateStamp = formatDateStamp(resolvedNowMs, timezone); const maxContextChars =
resolveAgentContextLimits(cfg, agentId)?.postCompactionMaxChars ?? MAX_CONTEXT_CHARS; // Always append the real runtime timestamp — AGENTS.md content may itself contain // "Current time:" as user-authored text, so we must not gate on that substring. const { timeLine } = resolveCronStyleNow(cfg ?? {}, resolvedNowMs);
// When using the default section set, use precise prose that names the // "Session Startup" sequence explicitly. When custom sections are configured, // use generic prose — referencing a hardcoded "Session Startup" sequence // would be misleading for deployments that use different section names. const prose = isDefaultSections
? "Session was just compacted. The conversation summary above is a hint, NOT a substitute for your startup sequence. " + "Run your Session Startup sequence - read the required files before responding to the user."
: `Session was just compacted. The conversation summary above is a hint, NOT a substitute for your full startup sequence. ` +
`Re-read the sections injected below (${displayNames.join(", ")}) and follow your configured startup procedure before responding to the user.`;
const sectionLabel = isDefaultSections
? "Critical rules from AGENTS.md:"
: `Injected sections from AGENTS.md (${displayNames.join(", ")}):`;
for (const name of sectionNames) {
let sectionLines: string[] = [];
let inSection = false;
let sectionLevel = 0;
let inCodeBlock = false;
for (const line of lines) { // Track fenced code blocks if (line.trimStart().startsWith("```")) {
inCodeBlock = !inCodeBlock; if (inSection) {
sectionLines.push(line);
} continue;
}
// Skip heading detection inside code blocks if (inCodeBlock) { if (inSection) {
sectionLines.push(line);
} continue;
}
// Check if this line is a heading const headingMatch = line.match(/^(#{2,3})\s+(.+?)\s*$/);
if (headingMatch) { const level = headingMatch[1].length; // 2 or 3 const headingText = headingMatch[2];
if (!inSection) { // Check if this is our target section (case-insensitive) if (
normalizeLowercaseStringOrEmpty(headingText) === normalizeLowercaseStringOrEmpty(name)
) {
inSection = true;
sectionLevel = level;
sectionLines = [line]; continue;
}
} else { // We're in section — stop if we hit a heading of same or higher level if (level <= sectionLevel) { break;
} // Lower-level heading (e.g., ### inside ##) — include it
sectionLines.push(line); continue;
}
}
if (inSection) {
sectionLines.push(line);
}
}
if (sectionLines.length > 0) {
results.push(sectionLines.join("\n").trim());
foundNames?.push(name);
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.