From fb0fe6b40befe25a3384d0cf6570195067dc56c2 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Sat, 21 Mar 2026 13:36:55 +1000 Subject: [PATCH] feat: add archive count limits and unify versions (v2.7.0) Co-Authored-By: Claude Opus 4.6 --- README.md | 3 ++- index.js | 55 ++++++++++++++++++++++++++++++++++++++++---- openclaw.plugin.json | 11 ++++++++- package.json | 2 +- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 25a1417..eb4fb0e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # memory-continuity -**Current release:** `v2.3.0` +**Current release:** `v2.7.0` OpenClaw **lifecycle plugin** for short-term working continuity. Preserves structured in-flight work state across `/new`, reset, gateway restarts, model fallback, and context compaction. @@ -122,6 +122,7 @@ The plugin works with zero configuration. Optional settings in `openclaw.json`: | `maxStateLines` | `50` | Max lines for CURRENT_STATE.md | | `archiveOnNew` | `true` | Archive state to `session_archive/` before `/new` | | `autoExtract` | `true` | Auto-extract state from conversation at session end | +| `maxArchiveCount` | `20` | Maximum archive files to keep (oldest auto-deleted) | ## The checkpoint file diff --git a/index.js b/index.js index b2f0ef8..a5eea98 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,9 @@ import path from "node:path"; // Constants // --------------------------------------------------------------------------- +const MAX_ARCHIVE_COUNT = 20; +const MAX_MEMORY_FILES = 500; + const PLACEHOLDER_VALUES = new Set([ "", "none", "n/a", "na", "idle", "[one sentence: what are we trying to accomplish]", @@ -82,13 +85,58 @@ function buildSnapshot(md) { ].join("\n"); } -function archiveState(workspaceDir, md) { +function archiveState(workspaceDir, md, config = {}) { const now = new Date(); const pad = (n) => String(n).padStart(2, "0"); const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`; const archiveDir = path.join(workspaceDir, "memory", "session_archive"); fs.mkdirSync(archiveDir, { recursive: true }); fs.writeFileSync(path.join(archiveDir, `${stamp}.md`), md, "utf8"); + + // Enforce archive count limit + const maxCount = config.maxArchiveCount || MAX_ARCHIVE_COUNT; + const files = fs.readdirSync(archiveDir).sort(); + if (files.length > maxCount) { + const toDelete = files.slice(0, files.length - maxCount); + for (const f of toDelete) { + try { fs.unlinkSync(path.join(archiveDir, f)); } catch {} + } + } + + // Clean up memory directory + cleanupMemoryDir(workspaceDir); +} + +function cleanupMemoryDir(workspaceDir) { + const memoryDir = path.join(workspaceDir, "memory"); + try { + const entries = fs.readdirSync(memoryDir); + + // Clean up legacy STATE_ARCHIVE_*.md files from memory/ root + const legacyArchives = entries.filter(f => /^STATE_ARCHIVE_.*\.md$/.test(f)); + for (const f of legacyArchives) { + try { fs.unlinkSync(path.join(memoryDir, f)); } catch {} + } + + // Check total file count (top level only) + const remaining = fs.readdirSync(memoryDir); + const fileEntries = remaining.filter(f => { + try { return fs.statSync(path.join(memoryDir, f)).isFile(); } catch { return false; } + }); + + if (fileEntries.length > MAX_MEMORY_FILES) { + const PROTECTED = new Set(["CURRENT_STATE.md", "MEMORY.md", "INDEX.md"]); + const deletable = fileEntries + .filter(f => !PROTECTED.has(f) && f.endsWith(".md")) + .map(f => ({ name: f, mtime: fs.statSync(path.join(memoryDir, f)).mtimeMs })) + .sort((a, b) => a.mtime - b.mtime); + + const toRemove = deletable.slice(0, fileEntries.length - 450); + for (const { name } of toRemove) { + try { fs.unlinkSync(path.join(memoryDir, name)); } catch {} + } + } + } catch {} } function extractStateFromMessages(messages) { @@ -222,7 +270,7 @@ const plugin = { if (!md) return; if (buildSnapshot(md)) { - archiveState(ws, md); + archiveState(ws, md, config); log.info?.("[memory-continuity] Archived state before /new"); } }, { priority: 10 }); @@ -267,8 +315,7 @@ const plugin = { // Archive previous state if it exists if (existing) { - const archivePath = statePath.replace(/CURRENT_STATE\.md$/, `STATE_ARCHIVE_${Date.now()}.md`); - writeFile(archivePath, existing); + archiveState(ws, existing, config); } writeFile(statePath, newState); diff --git a/openclaw.plugin.json b/openclaw.plugin.json index ba5100b..5b08c20 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "memory-continuity", "name": "Memory Continuity", "description": "Preserves working state across /new, reset, compaction, and gateway restarts via a simple markdown checkpoint file.", - "version": "2.6.1", + "version": "2.7.0", "source": "https://github.com/dtzp555-max/memory-continuity", "configSchema": { "type": "object", @@ -22,6 +22,11 @@ "type": "boolean", "default": true, "description": "Auto-extract working state from conversation at agent_end" + }, + "maxArchiveCount": { + "type": "number", + "default": 20, + "description": "Maximum number of archive files to keep in session_archive/" } } }, @@ -37,6 +42,10 @@ "autoExtract": { "label": "Auto-extract state", "help": "Automatically save working state when conversation ends" + }, + "maxArchiveCount": { + "label": "Max archive files", + "help": "Old archives are auto-deleted when this limit is reached" } } } \ No newline at end of file diff --git a/package.json b/package.json index f8ac410..bba4d55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "memory-continuity", - "version": "2.5.0", + "version": "2.7.0", "description": "Zero-dependency memory continuity for OpenClaw — plain markdown, lifecycle hooks, no vector DB.", "main": "index.js", "type": "module",