feat: add memory file count hard limit (500) and archive cap (20)

Prevents memory/ from growing unbounded by capping STATE_ARCHIVE
files at 20 per type and enforcing a 500-file hard limit on the
entire memory directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 12:02:17 +10:00
co-authored by Claude Opus 4.6
parent 9ad4f0be03
commit cfb2de72d6
4 changed files with 114 additions and 25 deletions
+16
View File
@@ -130,6 +130,22 @@ Update the file by **overwriting** it, not appending, at these moments:
| Before handoff / subagent exit | Preserves outputs and unsurfaced results |
| After a substantive state change | Keeps checkpoint aligned with actual work |
### Override rule
**CURRENT_STATE.md must always be overwritten when:**
- A new task or objective starts — regardless of what is currently in the file
- The previous objective is complete or abandoned
- The user gives a new task that supersedes the previous one
Having content in CURRENT_STATE.md does NOT mean it should be preserved.
Content only matters if Objective is still active and work is genuinely in progress.
Checking before overwrite:
- Read the file
- If Objective matches the current task → update in place (overwrite)
- If Objective does NOT match → overwrite the entire file with the new state
- Never append. Never skip the update because "there's already something there".
### 4. Keep the checkpoint small
`CURRENT_STATE.md` should usually stay under about 40 lines and be readable in
@@ -1,6 +1,18 @@
import fs from "node:fs";
import path from "node:path";
const MAX_ARCHIVE_COUNT = 20;
const MAX_MEMORY_FILES = 500;
// Protected files that should never be auto-deleted
const PROTECTED_FILES = new Set([
"CURRENT_STATE.md",
"MEMORY.md",
"INDEX.md",
"dev_principles.md",
"oracle_cloud.md",
]);
const PLACEHOLDER_VALUES = new Set([
"",
"none",
@@ -82,11 +94,70 @@ function formatArchiveStamp(date = new Date()) {
].join("-") + `_${pad(date.getHours())}-${pad(date.getMinutes())}`;
}
function pruneArchives(dir: string, pattern: RegExp, max: number) {
try {
const files = fs.readdirSync(dir)
.filter((f: string) => pattern.test(f))
.sort();
const excess = files.length - max;
if (excess > 0) {
for (let i = 0; i < excess; i++) {
fs.unlinkSync(path.join(dir, files[i]));
}
console.log(`[memory-continuity] pruned ${excess} old archive(s) in ${dir}`);
}
} catch {}
}
function enforceFileLimit(memoryDir: string) {
try {
const allFiles = fs.readdirSync(memoryDir, { recursive: true }) as string[];
// Only count files, not directories
const fileList = allFiles
.map((f: string) => ({ name: String(f), full: path.join(memoryDir, String(f)) }))
.filter((f) => {
try { return fs.statSync(f.full).isFile(); } catch { return false; }
});
if (fileList.length <= MAX_MEMORY_FILES) return;
// Sort deletable files by mtime (oldest first), skip protected
const deletable = fileList
.filter((f) => {
const base = path.basename(f.name);
if (PROTECTED_FILES.has(base)) return false;
if (base.endsWith(".json")) return false; // automation state
return true;
})
.map((f) => ({ ...f, mtime: fs.statSync(f.full).mtimeMs }))
.sort((a, b) => a.mtime - b.mtime);
const toDelete = fileList.length - 450; // bring down to 450 for headroom
const deleted = Math.min(toDelete, deletable.length);
for (let i = 0; i < deleted; i++) {
fs.unlinkSync(deletable[i].full);
}
if (deleted > 0) {
console.log(`[memory-continuity] enforced file limit: deleted ${deleted} oldest files (${fileList.length}${fileList.length - deleted})`);
}
} catch {}
}
function appendArchive(workspaceDir: string, markdown: string) {
const stamp = formatArchiveStamp();
const archiveDir = path.join(workspaceDir, "memory", "session_archive");
fs.mkdirSync(archiveDir, { recursive: true });
fs.writeFileSync(path.join(archiveDir, `${stamp}.md`), markdown, "utf8");
// Prune session_archive/ beyond MAX_ARCHIVE_COUNT
pruneArchives(archiveDir, /\.md$/, MAX_ARCHIVE_COUNT);
// Also prune STATE_ARCHIVE_*.md in memory/ root (generated by OpenClaw native)
const memoryDir = path.join(workspaceDir, "memory");
pruneArchives(memoryDir, /^STATE_ARCHIVE_.*\.md$/, MAX_ARCHIVE_COUNT);
// Hard cap: total files in memory/ must not exceed MAX_MEMORY_FILES
enforceFileLimit(memoryDir);
}
export default function register(api: any) {