mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-22 13:35:06 +00:00
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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user