feat: add archive count limits and unify versions (v2.7.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 13:36:55 +10:00
co-authored by Claude Opus 4.6
parent 2184442bc2
commit fb0fe6b40b
4 changed files with 64 additions and 7 deletions
+2 -1
View File
@@ -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
+51 -4
View File
@@ -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);
+10 -1
View File
@@ -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"
}
}
}
+1 -1
View File
@@ -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",