mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-19 09:42:42 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user