Files
openclaw/src/auto-reply/reply/post-compaction-audit.ts
康熙 65a1787f92 fix: normalize paths to forward slashes for Windows RegExp compatibility
Windows path.relative() produces backslashes (e.g., memory\2026-02-16.md)
which fail to match RegExp patterns using forward slashes.

Normalize relative paths to forward slashes before RegExp matching
using rel.split(path.sep).join('/').

Fixes 4 test failures on Windows CI.
2026-02-17 00:00:20 +01:00

112 lines
3.5 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
// Default required files — constants, extensible to config later
const DEFAULT_REQUIRED_READS: Array<string | RegExp> = [
"WORKFLOW_AUTO.md",
/memory\/\d{4}-\d{2}-\d{2}\.md/, // daily memory files
];
/**
* Audit whether agent read required startup files after compaction.
* Returns list of missing file patterns.
*/
export function auditPostCompactionReads(
readFilePaths: string[],
workspaceDir: string,
requiredReads: Array<string | RegExp> = DEFAULT_REQUIRED_READS,
): { passed: boolean; missingPatterns: string[] } {
const normalizedReads = readFilePaths.map((p) => path.resolve(workspaceDir, p));
const missingPatterns: string[] = [];
for (const required of requiredReads) {
if (typeof required === "string") {
const requiredResolved = path.resolve(workspaceDir, required);
const found = normalizedReads.some((r) => r === requiredResolved);
if (!found) {
missingPatterns.push(required);
}
} else {
// RegExp — match against relative paths from workspace
const found = readFilePaths.some((p) => {
const rel = path.relative(workspaceDir, path.resolve(workspaceDir, p));
// Normalize to forward slashes for cross-platform RegExp matching
const normalizedRel = rel.split(path.sep).join("/");
return required.test(normalizedRel);
});
if (!found) {
missingPatterns.push(required.source);
}
}
}
return { passed: missingPatterns.length === 0, missingPatterns };
}
/**
* Read messages from a session JSONL file.
* Returns messages from the last N lines (default 100).
*/
export function readSessionMessages(
sessionFile: string,
maxLines = 100,
): Array<{ role?: string; content?: unknown }> {
if (!fs.existsSync(sessionFile)) {
return [];
}
try {
const content = fs.readFileSync(sessionFile, "utf-8");
const lines = content.trim().split("\n");
const recentLines = lines.slice(-maxLines);
const messages: Array<{ role?: string; content?: unknown }> = [];
for (const line of recentLines) {
try {
const entry = JSON.parse(line);
if (entry.type === "message" && entry.message) {
messages.push(entry.message);
}
} catch {
// Skip malformed lines
}
}
return messages;
} catch {
return [];
}
}
/**
* Extract file paths from Read tool calls in agent messages.
* Looks for tool_use blocks with name="read" and extracts path/file_path args.
*/
export function extractReadPaths(messages: Array<{ role?: string; content?: unknown }>): string[] {
const paths: string[] = [];
for (const msg of messages) {
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
continue;
}
for (const block of msg.content) {
if (block.type === "tool_use" && block.name === "read") {
const filePath = block.input?.file_path ?? block.input?.path;
if (typeof filePath === "string") {
paths.push(filePath);
}
}
}
}
return paths;
}
/** Format the audit warning message */
export function formatAuditWarning(missingPatterns: string[]): string {
const fileList = missingPatterns.map((p) => ` - ${p}`).join("\n");
return (
"⚠️ Post-Compaction Audit: The following required startup files were not read after context reset:\n" +
fileList +
"\n\nPlease read them now using the Read tool before continuing. " +
"This ensures your operating protocols are restored after memory compaction."
);
}