Skip to content

Instantly share code, notes, and snippets.

@CypherpunkSamurai
Forked from joe-rlo/README.md
Created February 16, 2026 02:26
Show Gist options
  • Select an option

  • Save CypherpunkSamurai/95f6eef166d7bdd16b95c8269fac2b34 to your computer and use it in GitHub Desktop.

Select an option

Save CypherpunkSamurai/95f6eef166d7bdd16b95c8269fac2b34 to your computer and use it in GitHub Desktop.
Memory Guardian - OpenClaw plugin for persistent agent memory

Memory Guardian Plugin for OpenClaw

Fixes the "my bot keeps forgetting stuff" problem through active enforcement, not passive suggestions.

The Problem

  • AGENTS.md says "read your notes" but agents ignore it under attention pressure
  • Sessions get compacted/reset and context is lost
  • Multiple sessions (Telegram topics, Discord channels) don't share state
  • Agents forget to checkpoint after doing work

The Solution

A plugin that hooks into OpenClaw's extension system:

Hook What it does
before_agent_start Injects today's + yesterday's notes into every session
before_agent_start Escalating checkpoint reminders (12→20→30 tool calls)
before_compaction Writes breadcrumb before context compression
before_reset Writes breadcrumb before session reset
after_tool_call Tracks memory_search calls, nags after 8 turns
after_tool_call Updates shared state so sessions see each other's work
after_tool_call Auto-breadcrumbs every 10 tool calls as fallback

Installation

```bash

1. Create the extension directory

mkdir -p /.openclaw/extensions/memory-guardian

2. Download the plugin

curl -o /.openclaw/extensions/memory-guardian/index.js
https://gist.githubusercontent.com/joe-rlo/3c3193285804b05c99bbfe541ed53c4d/raw/48c2cb3471df0cd47cc5f3c95feb2e180b243953/index.js

3. Restart OpenClaw — plugin auto-discovered

openclaw gateway restart ```

Configuration

Edit the CONFIG object at the top of index.js:

```javascript const CONFIG = { maxDailyNotesChars: 4000, // Max chars to inject from daily notes maxSharedStateLines: 20, // Lines in cross-session state memorySearchReminderTurns: 8, // Turns before "you haven't searched" nag breadcrumbInterval: 10, // Auto-breadcrumb every N tool calls checkpointGentle: 12, // "Consider checkpointing" checkpointFirm: 20, // "You need to checkpoint" checkpointUrgent: 30, // "STOP. Checkpoint NOW." }; ```

Requirements

  • OpenClaw v0.4.0+ (plugin hook system)
  • A memory/ directory in your workspace for daily notes

How It Works

Context Injection

Every agent turn, the plugin injects:

  • Today's daily notes (memory/YYYY-MM-DD.md)
  • Yesterday's daily notes
  • Cross-session shared state
  • Any compaction/reset alerts
  • Checkpoint reminders if overdue

Checkpoint Gate

Tracks tool calls since last write to memory/*.md:

  • 12 calls: gentle reminder
  • 20 calls: firm warning
  • 30 calls: STOP instruction injected into context

Cross-Session State

When any session writes a file, it appends a line to memory/shared-state.md: ```

  • 14:32 [telegram:123] Write: my-file.ts
  • 14:35 [discord:456] Edit: config.json ```

Other sessions see this, bridging isolated contexts.

Auto-Breadcrumbs

Every 10 tool calls, auto-appends to daily notes: ```markdown

Auto-Breadcrumb (14:45 UTC)

  • Session: telegram:main
  • Tool calls since checkpoint: 15
  • Recent: exec, Write:app.tsx, Read:config.ts ```

Even if the agent never checkpoints, there's a trail.

License

MIT — do whatever you want with it.


Built by @OnlyAMicrowave and @joespano_

/**
* Memory Guardian Plugin for OpenClaw
* ====================================
*
* Fixes the "my bot keeps forgetting stuff" problem through active enforcement,
* not passive suggestions. AGENTS.md alone doesn't work under attention pressure.
*
* INSTALLATION
* ------------
* 1. Create directory: mkdir -p <your-workspace>/.openclaw/extensions/memory-guardian
* 2. Copy this file to: <your-workspace>/.openclaw/extensions/memory-guardian/index.js
* 3. Restart OpenClaw — plugin auto-discovered, no config needed
*
* LICENSE: MIT — do whatever you want with it
*/
import fs from "node:fs";
import path from "node:path";
const CONFIG = {
maxDailyNotesChars: 4000,
maxSharedStateLines: 20,
memorySearchReminderTurns: 8,
breadcrumbInterval: 10,
checkpointGentle: 12,
checkpointFirm: 20,
checkpointUrgent: 30,
};
const STATE_FILE = ".memory-guardian-state.json";
const COMPACTION_MARKER = ".compaction-pending";
const RESET_MARKER = ".reset-pending";
const SHARED_STATE_FILE = "shared-state.md";
const todayStr = () => new Date().toISOString().slice(0, 10);
const yesterdayStr = () => {
const d = new Date();
d.setDate(d.getDate() - 1);
return d.toISOString().slice(0, 10);
};
const timeStr = () => new Date().toISOString().slice(11, 16);
const memoryDir = (workspaceDir) => path.join(workspaceDir, "memory");
function ensureMemoryDir(workspaceDir) {
const dir = memoryDir(workspaceDir);
try { fs.mkdirSync(dir, { recursive: true }); } catch { }
return dir;
}
function readFileSafe(filePath, maxChars) {
try {
if (!fs.existsSync(filePath)) return null;
let content = fs.readFileSync(filePath, "utf-8");
if (maxChars && content.length > maxChars) {
content = content.slice(0, maxChars) + "\n\n[... truncated at " + maxChars + " chars ...]";
}
return content;
} catch { return null; }
}
function writeJsonSafe(filePath, data) {
try { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); return true; }
catch { return false; }
}
function readJsonSafe(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch { return null; }
}
function unlinkSafe(filePath) {
try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch { }
}
function appendToFile(filePath, text) {
try { fs.appendFileSync(filePath, text); return true; }
catch { return false; }
}
function getWorkspaceDir(ctx, config) {
return ctx?.workspaceDir || config?.agents?.defaults?.workspaceDir || process.cwd();
}
function defaultState() {
return {
turnsSinceMemorySearch: 0,
lastMemorySearchTime: null,
lastCheckpointTime: null,
totalTurns: 0,
toolCallsSinceCheckpoint: 0,
toolCallsSinceBreadcrumb: 0,
recentTools: [],
};
}
function readState(workspaceDir) {
const statePath = path.join(memoryDir(workspaceDir), STATE_FILE);
return { ...defaultState(), ...readJsonSafe(statePath) };
}
function writeState(workspaceDir, state) {
const dir = ensureMemoryDir(workspaceDir);
writeJsonSafe(path.join(dir, STATE_FILE), state);
}
export default function memoryGuardian(api) {
const log = api.logger;
api.on("before_agent_start", (event, ctx) => {
const workspaceDir = getWorkspaceDir(ctx, api.config);
const memDir = memoryDir(workspaceDir);
const parts = [];
const compactionData = readJsonSafe(path.join(memDir, COMPACTION_MARKER));
if (compactionData) {
parts.push(
`⚠️ MEMORY ALERT: Session was compacted at ${compactionData.timestamp}. ` +
`Run memory_search and check memory/${todayStr()}.md for gaps.`
);
unlinkSafe(path.join(memDir, COMPACTION_MARKER));
}
const resetData = readJsonSafe(path.join(memDir, RESET_MARKER));
if (resetData) {
parts.push(
`⚠️ MEMORY ALERT: Session was reset at ${resetData.timestamp}. ` +
`Review memory/${todayStr()}.md for gaps.`
);
unlinkSafe(path.join(memDir, RESET_MARKER));
}
const state = readState(workspaceDir);
if (state.turnsSinceMemorySearch >= CONFIG.memorySearchReminderTurns) {
parts.push(
`📝 MEMORY REMINDER: You haven't called memory_search in ${state.turnsSinceMemorySearch} turns. ` +
`If you're about to answer a question about past work, decisions, or preferences — search first.`
);
}
const toolsSinceCP = state.toolCallsSinceCheckpoint || 0;
if (toolsSinceCP >= CONFIG.checkpointUrgent) {
parts.push(
`🚨 CHECKPOINT OVERDUE (${toolsSinceCP} tool calls without writing to memory):\n` +
`STOP. Before doing ANYTHING else, write a checkpoint to memory/${todayStr()}.md.\n` +
`Include: what you've been working on, key decisions, current state, next steps.\n` +
`This is not optional. Write the checkpoint NOW.`
);
} else if (toolsSinceCP >= CONFIG.checkpointFirm) {
parts.push(`⚠️ CHECKPOINT NEEDED (${toolsSinceCP} tool calls without writing to memory):\nWrite a checkpoint to memory/${todayStr()}.md soon.`);
} else if (toolsSinceCP >= CONFIG.checkpointGentle) {
parts.push(`📋 Consider checkpointing — ${toolsSinceCP} tool calls since your last memory write.`);
}
const todayNotes = readFileSafe(path.join(memDir, `${todayStr()}.md`), CONFIG.maxDailyNotesChars);
if (todayNotes) parts.push(`## Today's Notes (${todayStr()})\n${todayNotes}`);
const yesterdayNotes = readFileSafe(path.join(memDir, `${yesterdayStr()}.md`), CONFIG.maxDailyNotesChars);
if (yesterdayNotes) parts.push(`## Yesterday's Notes (${yesterdayStr()})\n${yesterdayNotes}`);
const sharedState = readFileSafe(path.join(memDir, SHARED_STATE_FILE), 2000);
if (sharedState) parts.push(`## Cross-Session State\n${sharedState}`);
state.totalTurns += 1;
state.turnsSinceMemorySearch += 1;
writeState(workspaceDir, state);
if (parts.length === 0) return;
return { prependContext: "# Memory Guardian Context\n\n" + parts.join("\n\n") };
}, { priority: 50 });
api.on("before_compaction", (event, ctx) => {
const workspaceDir = getWorkspaceDir(ctx, api.config);
const dir = ensureMemoryDir(workspaceDir);
const state = readState(workspaceDir);
const data = {
timestamp: new Date().toISOString(),
sessionKey: ctx?.sessionKey || "unknown",
messageCount: event?.messageCount || 0,
toolCallsSinceCheckpoint: state.toolCallsSinceCheckpoint || 0,
};
writeJsonSafe(path.join(dir, COMPACTION_MARKER), data);
const breadcrumb = `\n## Auto-Breadcrumb: Pre-Compaction (${timeStr()} UTC)\n` +
`- Messages: ${data.messageCount}\n` +
`- Tool calls since checkpoint: ${data.toolCallsSinceCheckpoint}\n` +
`- ⚠️ Compaction happening — context about to be compressed\n`;
appendToFile(path.join(dir, `${todayStr()}.md`), breadcrumb);
log.info(`memory-guardian: compaction marker written`);
}, { priority: 100 });
api.on("before_reset", (event, ctx) => {
const workspaceDir = getWorkspaceDir(ctx, api.config);
const dir = ensureMemoryDir(workspaceDir);
const state = readState(workspaceDir);
const data = {
timestamp: new Date().toISOString(),
sessionKey: ctx?.sessionKey || "unknown",
reason: event?.reason || "manual",
toolCallsSinceCheckpoint: state.toolCallsSinceCheckpoint || 0,
};
writeJsonSafe(path.join(dir, RESET_MARKER), data);
const breadcrumb = `\n## Auto-Breadcrumb: Pre-Reset (${timeStr()} UTC)\n` +
`- Reason: ${data.reason}\n` +
`- Tool calls since checkpoint: ${data.toolCallsSinceCheckpoint}\n` +
`- ⚠️ Session being reset — all context will be lost\n`;
appendToFile(path.join(dir, `${todayStr()}.md`), breadcrumb);
log.info(`memory-guardian: reset marker written`);
}, { priority: 100 });
api.on("after_tool_call", (event, ctx) => {
const toolName = event?.toolName || event?.name || "";
if (toolName !== "memory_search") return;
const workspaceDir = getWorkspaceDir(ctx, api.config);
const state = readState(workspaceDir);
state.turnsSinceMemorySearch = 0;
state.lastMemorySearchTime = new Date().toISOString();
writeState(workspaceDir, state);
}, { priority: 10 });
api.on("after_tool_call", (event, ctx) => {
const toolName = event?.toolName || event?.name || "";
if (!toolName) return;
const workspaceDir = getWorkspaceDir(ctx, api.config);
const state = readState(workspaceDir);
const params = event?.params || event?.arguments || {};
const target = params?.file_path || params?.path || "";
state.toolCallsSinceCheckpoint = (state.toolCallsSinceCheckpoint || 0) + 1;
state.toolCallsSinceBreadcrumb = (state.toolCallsSinceBreadcrumb || 0) + 1;
if (!Array.isArray(state.recentTools)) state.recentTools = [];
const shortTarget = typeof target === "string" ? target.split("/").slice(-1)[0] : "";
state.recentTools.push(`${toolName}${shortTarget ? ":" + shortTarget : ""}`);
if (state.recentTools.length > 10) state.recentTools = state.recentTools.slice(-10);
const isMemoryWrite =
["Write", "write", "Edit", "edit"].includes(toolName) &&
typeof target === "string" &&
target.includes("memory/") &&
target.endsWith(".md");
if (isMemoryWrite) {
state.toolCallsSinceCheckpoint = 0;
state.toolCallsSinceBreadcrumb = 0;
state.lastCheckpointTime = new Date().toISOString();
writeState(workspaceDir, state);
return;
}
if (["write", "Write", "edit", "Edit"].includes(toolName)) {
const dir = ensureMemoryDir(workspaceDir);
const sharedStatePath = path.join(dir, SHARED_STATE_FILE);
const sessionLabel = (ctx?.sessionKey || "unknown").split(":").slice(-2).join(":");
const line = `- ${timeStr()} [${sessionLabel}] ${toolName}: ${shortTarget}`;
try {
let lines = fs.existsSync(sharedStatePath)
? fs.readFileSync(sharedStatePath, "utf-8").split("\n").filter(Boolean)
: [];
lines.push(line);
if (lines.length > CONFIG.maxSharedStateLines) {
lines = lines.slice(-CONFIG.maxSharedStateLines);
}
fs.writeFileSync(sharedStatePath, lines.join("\n") + "\n");
} catch { }
}
if (state.toolCallsSinceBreadcrumb >= CONFIG.breadcrumbInterval) {
const dir = ensureMemoryDir(workspaceDir);
const sessionLabel = (ctx?.sessionKey || "unknown").split(":").slice(-2).join(":");
const breadcrumb =
`\n### Auto-Breadcrumb (${timeStr()} UTC)\n` +
`- Session: ${sessionLabel}\n` +
`- Tool calls since checkpoint: ${state.toolCallsSinceCheckpoint}\n` +
`- Recent: ${state.recentTools.slice(-5).join(", ")}\n`;
appendToFile(path.join(dir, `${todayStr()}.md`), breadcrumb);
state.toolCallsSinceBreadcrumb = 0;
}
writeState(workspaceDir, state);
}, { priority: 8 });
log.info("memory-guardian: loaded — context injection + checkpoint gate + tracking");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment