mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-19 09:42:42 +00:00
feat: upgrade from skill to lifecycle plugin (v2.0.0)
Replace prompt-based skill approach with OpenClaw lifecycle hooks (before_agent_start, agent_end, before_compaction, before_reset). State injection now happens automatically at the hook level, making it model-agnostic and reliable across all providers. - Add index.js with 5 lifecycle hooks - Add openclaw.plugin.json manifest - Rewrite post-install.sh to handle plugin installation - Update README with plugin architecture docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,110 +1,106 @@
|
||||
# memory-continuity
|
||||
|
||||
**Current release:** `v0.3.0-probe`
|
||||
**Current release:** `v2.0.0`
|
||||
|
||||
OpenClaw continuity package for **short-term working continuity** — currently shipped as:
|
||||
- a **skill** (`SKILL.md`) for behavior contract / fallback recovery
|
||||
- a **lifecycle plugin probe** (`plugin/lifecycle-prototype.ts`) for validating the primary runtime path
|
||||
|
||||
Its goal is to let an agent recover structured in-flight work state after `/new`, reset, gateway interruption, model fallback, or compaction.
|
||||
OpenClaw **lifecycle plugin** for short-term working continuity. Preserves structured in-flight work state across `/new`, reset, gateway restarts, model fallback, and context compaction.
|
||||
|
||||
## What problem does this solve?
|
||||
|
||||
OpenClaw already preserves a lot:
|
||||
- transcripts
|
||||
- compaction summaries
|
||||
- memory files
|
||||
- session memory search
|
||||
|
||||
But those do not always answer the most operational question:
|
||||
OpenClaw already preserves transcripts, compaction summaries, memory files, and session memory search. But those don't always answer the most operational question:
|
||||
|
||||
> What were we doing right now, where did we stop, and what should happen next?
|
||||
|
||||
That is the problem this skill solves.
|
||||
That is the problem this plugin solves.
|
||||
|
||||
**One-line summary:**
|
||||
- long-term memory = what you know
|
||||
- memory continuity = what you are doing right now
|
||||
|
||||
## Current architecture stance
|
||||
## How it works
|
||||
|
||||
> **Alpha support boundary (`v0.3.0-probe`)**
|
||||
>
|
||||
> Currently validated:
|
||||
> - resident subagent startup continuity
|
||||
>
|
||||
> Not currently supported / not yet validated for reliable recovery:
|
||||
> - Discord main/channel/thread continuity
|
||||
>
|
||||
The plugin uses OpenClaw lifecycle hooks to **automatically** save and restore working state — no model cooperation needed.
|
||||
|
||||
This repository should now be understood as a **continuity package**, not just a standalone skill.
|
||||
| Hook | What it does |
|
||||
|---|---|
|
||||
| `before_agent_start` | Reads `memory/CURRENT_STATE.md` and injects it into the agent's system context |
|
||||
| `before_compaction` | Injects state before compaction so it survives context compression |
|
||||
| `before_reset` | Archives current state to `session_archive/` before `/new` |
|
||||
| `agent_end` | Auto-extracts working state from conversation if no explicit state exists |
|
||||
| `session_end` | Ensures `CURRENT_STATE.md` exists for future sessions |
|
||||
|
||||
### Included forms
|
||||
- **Skill** = behavior contract / fallback implementation / human-readable protocol
|
||||
- **Lifecycle plugin probe** = current runtime experiment for the primary architecture
|
||||
|
||||
The intended primary runtime path is a **standard lifecycle plugin** that can
|
||||
improve startup, `/new`, and compaction continuity **without consuming
|
||||
OpenClaw’s exclusive `contextEngine` slot**.
|
||||
|
||||
A ContextEngine implementation remains a **future option**, not the default
|
||||
v1 direction.
|
||||
Because state injection happens at the hook level (before the model sees anything), it works with **any model** — GPT-4o, MiniMax, Claude, etc.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/main/skills/
|
||||
# Clone the plugin
|
||||
git clone https://github.com/dtzp555-max/memory-continuity.git
|
||||
|
||||
# Run the installer
|
||||
cd memory-continuity
|
||||
bash scripts/post-install.sh
|
||||
```
|
||||
|
||||
The installer will:
|
||||
1. Copy the plugin to `~/.openclaw/extensions/memory-continuity/`
|
||||
2. Add the plugin entry to `~/.openclaw/openclaw.json`
|
||||
3. Restart the gateway
|
||||
|
||||
No npm install, no API keys, no external database.
|
||||
|
||||
> **Why `post-install.sh`?**
|
||||
> OpenClaw caches each session's skill list in a `skillsSnapshot`. If you
|
||||
> install this skill while the gateway is stopped (or restart the gateway
|
||||
> after cloning), existing sessions won't detect the new skill until their
|
||||
> snapshot is cleared. The post-install script handles this automatically.
|
||||
> New sessions created after install are unaffected.
|
||||
|
||||
### Test the current skill version
|
||||
|
||||
1. Start a multi-step task with your agent
|
||||
2. Make a few concrete decisions
|
||||
3. Check whether `memory/CURRENT_STATE.md` exists and reflects the work state
|
||||
4. Trigger `/new`
|
||||
5. Ask a recovery question like:
|
||||
- “刚才我们说到哪了”
|
||||
- “continue”
|
||||
- “what were we doing”
|
||||
|
||||
A good recovery should surface the current objective / step / next action,
|
||||
not generic small talk.
|
||||
|
||||
### Run the doctor
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
python3 scripts/continuity_doctor.py --workspace ~/.openclaw/workspace
|
||||
openclaw gateway restart 2>&1 | grep memory-continuity
|
||||
# Should show: [memory-continuity] Plugin registered successfully
|
||||
```
|
||||
|
||||
## How the current skill version works
|
||||
### Test
|
||||
|
||||
The skill defines a discipline around one file:
|
||||
- `memory/CURRENT_STATE.md`
|
||||
1. Tell your agent something memorable (e.g., "I'll tell you a secret: Ethan is super kid")
|
||||
2. Send `/new` to reset the session
|
||||
3. Ask "what was the secret?" or "我们刚才聊到哪了"
|
||||
4. The agent should immediately surface the recovered state
|
||||
|
||||
That file is the short-term workbench for active work. It is:
|
||||
- overwritten, not appended
|
||||
- intentionally short
|
||||
- structured for fast recovery
|
||||
## Configuration
|
||||
|
||||
### The checkpoint shape
|
||||
The plugin works with zero configuration. Optional settings in `openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"memory-continuity": {
|
||||
"enabled": true,
|
||||
"hooks": {
|
||||
"allowPromptInjection": true
|
||||
},
|
||||
"config": {
|
||||
"maxStateLines": 50,
|
||||
"archiveOnNew": true,
|
||||
"autoExtract": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `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 |
|
||||
|
||||
## The checkpoint file
|
||||
|
||||
The plugin maintains one file: `$WORKSPACE/memory/CURRENT_STATE.md`
|
||||
|
||||
```markdown
|
||||
# Current State
|
||||
> Last updated: 2026-03-12T14:30:00Z
|
||||
> Last updated: 2026-03-15T14:00:00Z
|
||||
|
||||
## Objective
|
||||
Build the user authentication module
|
||||
@@ -114,7 +110,6 @@ Completed JWT token generation, starting refresh endpoint
|
||||
|
||||
## Key Decisions
|
||||
- Using RS256 for token signing (user approved)
|
||||
- Token expiry: 15 minutes access, 7 days refresh
|
||||
|
||||
## Next Action
|
||||
Implement POST /auth/refresh endpoint
|
||||
@@ -126,103 +121,67 @@ None
|
||||
None
|
||||
```
|
||||
|
||||
## Recovery rules
|
||||
This file is:
|
||||
- **Overwritten**, not appended (it's a checkpoint, not a journal)
|
||||
- **Human-readable** plain markdown
|
||||
- **Portable** — just copy the file to backup or migrate
|
||||
- **Model-agnostic** — injected via hooks, not dependent on model behavior
|
||||
|
||||
In recovery scenarios, the skill expects the agent to prioritize:
|
||||
- Objective
|
||||
- Current Step
|
||||
- Next Action
|
||||
- Blockers
|
||||
- Unsurfaced Results
|
||||
## Backup & Migration
|
||||
|
||||
A generic greeting should **not** outrank recovery state when the checkpoint
|
||||
contains active work.
|
||||
```bash
|
||||
# Backup
|
||||
cp $WORKSPACE/memory/CURRENT_STATE.md /backup/
|
||||
|
||||
## Relationship to native OpenClaw features
|
||||
# Migrate to another machine
|
||||
scp -r ~/.openclaw/extensions/memory-continuity/ newhost:~/.openclaw/extensions/
|
||||
scp $WORKSPACE/memory/CURRENT_STATE.md newhost:$WORKSPACE/memory/
|
||||
```
|
||||
|
||||
### Native OpenClaw already handles
|
||||
- transcript persistence
|
||||
- compaction
|
||||
- pre-compaction `memoryFlush`
|
||||
- session memory search
|
||||
- system prompt/bootstrap assembly
|
||||
No database, no vector embeddings, no API keys to transfer.
|
||||
|
||||
### memory-continuity adds
|
||||
- a **structured working-state checkpoint**
|
||||
- explicit short-term recovery fields
|
||||
- a deterministic place to look for active work state
|
||||
- explicit handling for `Unsurfaced Results`
|
||||
## Architecture: Plugin vs Skill
|
||||
|
||||
### Important boundary
|
||||
Session memory search is useful for:
|
||||
- “what did we discuss before?”
|
||||
- “what decision was mentioned in a prior session?”
|
||||
Previous versions (v0.x) shipped as a **skill** — a markdown file that asked the model to read/write `CURRENT_STATE.md`. This was unreliable because models could ignore the instructions.
|
||||
|
||||
Memory continuity is for:
|
||||
- “what are we doing right now?”
|
||||
- “where did we stop?”
|
||||
- “what should happen next?”
|
||||
v2.0 is a **lifecycle plugin** that uses OpenClaw hooks. The key difference:
|
||||
|
||||
| | Skill (v0.x) | Plugin (v2.0) |
|
||||
|---|---|---|
|
||||
| State injection | Model must read the file | Hook injects automatically |
|
||||
| State saving | Model must write the file | Hook saves automatically |
|
||||
| Model dependency | Requires model cooperation | Model-agnostic |
|
||||
| Reliability | Varies by model | Consistent |
|
||||
|
||||
The skill (`SKILL.md`) is retained as documentation and fallback protocol.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```text
|
||||
memory-continuity/
|
||||
├── SKILL.md # Behavior contract / skill definition
|
||||
├── skill.json # Skill metadata for OpenClaw loader
|
||||
├── _meta.json # Workspace skill registry metadata
|
||||
├── openclaw.plugin.json # Plugin manifest
|
||||
├── index.js # Plugin entry point (hooks)
|
||||
├── SKILL.md # Behavior contract / protocol docs
|
||||
├── README.md
|
||||
├── LICENSE
|
||||
├── plugin/
|
||||
│ └── lifecycle-prototype.ts # Phase 2 probe / not production yet
|
||||
│ └── lifecycle-prototype.ts # Original prototype (reference)
|
||||
├── references/
|
||||
│ ├── template.md
|
||||
│ ├── doctor-spec.md
|
||||
│ └── phase2-hook-validation.md
|
||||
└── scripts/
|
||||
├── post-install.sh # Clears stale skill snapshots
|
||||
└── continuity_doctor.py
|
||||
```
|
||||
|
||||
At runtime, the skill works primarily with:
|
||||
|
||||
```text
|
||||
$WORKSPACE/
|
||||
└── memory/
|
||||
├── CURRENT_STATE.md
|
||||
└── session_archive/
|
||||
├── post-install.sh # Automated installer
|
||||
└── continuity_doctor.py # Health check
|
||||
```
|
||||
|
||||
## Design principles
|
||||
|
||||
1. **Files are the source of truth**
|
||||
2. **Structured checkpoint beats free-form recollection**
|
||||
3. **Recovery must prefer truth over confident guessing**
|
||||
4. **This complements native OpenClaw memory; it does not replace it**
|
||||
5. **Read access is helpful, but should not be the only long-term path**
|
||||
6. **The primary plugin direction should coexist with other ecosystem plugins such as `lossless-claw`**
|
||||
|
||||
## Current roadmap
|
||||
|
||||
### Phase 1
|
||||
Strengthen the current skill version:
|
||||
- tighten recovery behavior
|
||||
- tighten checkpoint discipline
|
||||
- improve doctor and docs
|
||||
|
||||
### Phase 2
|
||||
Build and validate a **standard lifecycle plugin** as the primary runtime path:
|
||||
- startup recovery behavior
|
||||
- `/new` checkpointing
|
||||
- compaction-boundary checkpointing
|
||||
- end-of-run safety writes
|
||||
- hook validation in real resident subagent sessions
|
||||
|
||||
### Future option
|
||||
Evaluate a ContextEngine variant later only if the slot tradeoff is justified.
|
||||
|
||||
## Release notes
|
||||
|
||||
See `CHANGELOG.md` for the current packaged milestone history.
|
||||
1. **Files are the source of truth** — plain markdown, no database
|
||||
2. **Hooks over prompts** — don't rely on model behavior
|
||||
3. **Zero external dependencies** — no API keys, no vector DB
|
||||
4. **Portable and backupable** — `cp` is your backup tool
|
||||
5. **Complements native OpenClaw memory** — does not replace it
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PLACEHOLDER_VALUES = new Set([
|
||||
"", "none", "n/a", "na", "idle",
|
||||
"[one sentence: what are we trying to accomplish]",
|
||||
"[exactly what should happen next]",
|
||||
]);
|
||||
|
||||
const STATE_TEMPLATE = `# Current State
|
||||
> Last updated: ${new Date().toISOString()}
|
||||
|
||||
## Objective
|
||||
None
|
||||
|
||||
## Current Step
|
||||
None
|
||||
|
||||
## Key Decisions
|
||||
- None
|
||||
|
||||
## Next Action
|
||||
None
|
||||
|
||||
## Blockers
|
||||
None
|
||||
|
||||
## Unsurfaced Results
|
||||
None
|
||||
`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function resolveStatePath(workspaceDir) {
|
||||
if (!workspaceDir) return null;
|
||||
return path.join(workspaceDir, "memory", "CURRENT_STATE.md");
|
||||
}
|
||||
|
||||
function readFile(filePath) {
|
||||
try { return fs.readFileSync(filePath, "utf8"); } catch { return null; }
|
||||
}
|
||||
|
||||
function writeFile(filePath, content) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content, "utf8");
|
||||
}
|
||||
|
||||
function extractSection(md, heading) {
|
||||
const re = new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "m");
|
||||
return (md.match(re)?.[1] ?? "").trim();
|
||||
}
|
||||
|
||||
function isMeaningful(value) {
|
||||
return !PLACEHOLDER_VALUES.has(value.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function buildSnapshot(md) {
|
||||
const objective = extractSection(md, "Objective");
|
||||
if (!isMeaningful(objective)) return null;
|
||||
|
||||
const fields = {
|
||||
"Objective": objective,
|
||||
"Current Step": extractSection(md, "Current Step") || "unknown",
|
||||
"Key Decisions": extractSection(md, "Key Decisions") || "None",
|
||||
"Next Action": extractSection(md, "Next Action") || "unknown",
|
||||
"Blockers": extractSection(md, "Blockers") || "None",
|
||||
"Unsurfaced Results": extractSection(md, "Unsurfaced Results") || "None",
|
||||
};
|
||||
const updated = md.match(/^> Last updated:\s*(.+)$/m)?.[1]?.trim() ?? "unknown";
|
||||
|
||||
return [
|
||||
"=== CONTINUITY RECOVERY ===",
|
||||
...Object.entries(fields).map(([k, v]) => `${k}: ${v}`),
|
||||
`Last Updated: ${updated}`,
|
||||
"=== END RECOVERY ===",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function archiveState(workspaceDir, md) {
|
||||
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");
|
||||
}
|
||||
|
||||
function extractStateFromMessages(messages) {
|
||||
if (!messages || messages.length === 0) return null;
|
||||
|
||||
// Walk messages backwards to find the last meaningful exchange
|
||||
const userMessages = [];
|
||||
const assistantMessages = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
const role = msg?.role;
|
||||
const content = typeof msg?.content === "string"
|
||||
? msg.content
|
||||
: Array.isArray(msg?.content)
|
||||
? msg.content.filter(b => b?.type === "text").map(b => b.text).join("\n")
|
||||
: null;
|
||||
if (!content) continue;
|
||||
if (role === "user") userMessages.push(content);
|
||||
if (role === "assistant") assistantMessages.push(content);
|
||||
}
|
||||
|
||||
if (userMessages.length === 0 && assistantMessages.length === 0) return null;
|
||||
|
||||
// Build a simple state from the conversation tail
|
||||
const lastUser = userMessages[userMessages.length - 1] || "";
|
||||
const lastAssistant = assistantMessages[assistantMessages.length - 1] || "";
|
||||
|
||||
// Truncate to keep it compact
|
||||
const truncate = (s, max = 200) => s.length > max ? s.slice(0, max) + "..." : s;
|
||||
|
||||
return `# Current State
|
||||
> Last updated: ${new Date().toISOString()}
|
||||
|
||||
## Objective
|
||||
${truncate(lastUser, 300)}
|
||||
|
||||
## Current Step
|
||||
Conversation ended after ${messages.length} messages
|
||||
|
||||
## Key Decisions
|
||||
- Auto-extracted from conversation
|
||||
|
||||
## Next Action
|
||||
Continue from where we left off
|
||||
|
||||
## Blockers
|
||||
None
|
||||
|
||||
## Unsurfaced Results
|
||||
${truncate(lastAssistant, 500)}
|
||||
`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Definition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const plugin = {
|
||||
id: "memory-continuity",
|
||||
name: "Memory Continuity",
|
||||
|
||||
register(api) {
|
||||
const log = api.logger || console;
|
||||
const getWorkspace = () => api.runtime?.workspaceDir;
|
||||
const getConfig = () => api.pluginConfig || {};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HOOK 1: before_agent_start — inject recovered state into context
|
||||
// ------------------------------------------------------------------
|
||||
api.on("before_agent_start", async (_event, _ctx) => {
|
||||
const ws = getWorkspace();
|
||||
const statePath = resolveStatePath(ws);
|
||||
if (!statePath) return;
|
||||
|
||||
const md = readFile(statePath);
|
||||
if (!md) return;
|
||||
|
||||
const snapshot = buildSnapshot(md);
|
||||
if (!snapshot) return;
|
||||
|
||||
log.info?.("[memory-continuity] Injecting recovered state into context");
|
||||
|
||||
return {
|
||||
prependSystemContext:
|
||||
snapshot + "\n\n" +
|
||||
"IMPORTANT: The above is recovered working state from a previous session. " +
|
||||
"If the user appears to be resuming work, surface this state immediately " +
|
||||
"before any generic greeting. This is a continuity requirement, not optional.",
|
||||
};
|
||||
}, { priority: 10 });
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HOOK 2: before_compaction — inject state so it survives compaction
|
||||
// ------------------------------------------------------------------
|
||||
api.on("before_compaction", async (_event, _ctx) => {
|
||||
const ws = getWorkspace();
|
||||
const statePath = resolveStatePath(ws);
|
||||
if (!statePath) return;
|
||||
|
||||
const md = readFile(statePath);
|
||||
if (!md) return;
|
||||
|
||||
const snapshot = buildSnapshot(md);
|
||||
if (!snapshot) return;
|
||||
|
||||
log.info?.("[memory-continuity] Injecting state before compaction");
|
||||
|
||||
return {
|
||||
prependSystemContext: snapshot,
|
||||
};
|
||||
}, { priority: 10 });
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HOOK 3: before_reset (/new) — archive current state
|
||||
// ------------------------------------------------------------------
|
||||
api.on("before_reset", async (_event, _ctx) => {
|
||||
const ws = getWorkspace();
|
||||
const config = getConfig();
|
||||
if (!ws || config.archiveOnNew === false) return;
|
||||
|
||||
const statePath = resolveStatePath(ws);
|
||||
if (!statePath) return;
|
||||
|
||||
const md = readFile(statePath);
|
||||
if (!md) return;
|
||||
|
||||
if (buildSnapshot(md)) {
|
||||
archiveState(ws, md);
|
||||
log.info?.("[memory-continuity] Archived state before /new");
|
||||
}
|
||||
}, { priority: 10 });
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HOOK 4: agent_end — extract and save working state
|
||||
// ------------------------------------------------------------------
|
||||
api.on("agent_end", async (event, _ctx) => {
|
||||
const ws = getWorkspace();
|
||||
const config = getConfig();
|
||||
if (!ws || config.autoExtract === false) return;
|
||||
|
||||
const statePath = resolveStatePath(ws);
|
||||
if (!statePath) return;
|
||||
|
||||
// If there's already a meaningful state file, don't overwrite with
|
||||
// auto-extracted content (agent or user wrote it explicitly)
|
||||
const existing = readFile(statePath);
|
||||
if (existing && buildSnapshot(existing)) {
|
||||
log.info?.("[memory-continuity] Existing state is meaningful, skipping auto-extract");
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract from conversation messages
|
||||
const newState = extractStateFromMessages(event?.messages);
|
||||
if (!newState) return;
|
||||
|
||||
writeFile(statePath, newState);
|
||||
log.info?.("[memory-continuity] Auto-extracted state from conversation");
|
||||
}, { priority: 90 }); // low priority, run after other hooks
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HOOK 5: session_end — ensure state file exists
|
||||
// ------------------------------------------------------------------
|
||||
api.on("session_end", async (_event, _ctx) => {
|
||||
const ws = getWorkspace();
|
||||
const statePath = resolveStatePath(ws);
|
||||
if (!statePath) return;
|
||||
|
||||
if (!readFile(statePath)) {
|
||||
writeFile(statePath, STATE_TEMPLATE);
|
||||
log.info?.("[memory-continuity] Created initial CURRENT_STATE.md");
|
||||
}
|
||||
}, { priority: 90 });
|
||||
|
||||
log.info?.("[memory-continuity] Plugin registered successfully");
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"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.0.0",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"maxStateLines": {
|
||||
"type": "number",
|
||||
"default": 50,
|
||||
"description": "Max lines for CURRENT_STATE.md before auto-compress warning"
|
||||
},
|
||||
"archiveOnNew": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Archive CURRENT_STATE.md to session_archive/ on /new"
|
||||
},
|
||||
"autoExtract": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Auto-extract working state from conversation at agent_end"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uiHints": {
|
||||
"maxStateLines": { "label": "Max state file lines", "help": "Keep checkpoint small for fast recovery" },
|
||||
"archiveOnNew": { "label": "Archive on /new", "help": "Save a snapshot before session reset" },
|
||||
"autoExtract": { "label": "Auto-extract state", "help": "Automatically save working state when conversation ends" }
|
||||
}
|
||||
}
|
||||
+94
-49
@@ -1,71 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# post-install.sh — Flush stale skill snapshots after installing memory-continuity
|
||||
# post-install.sh — Install memory-continuity as an OpenClaw lifecycle plugin
|
||||
#
|
||||
# OpenClaw caches a "skillsSnapshot" per session. If this skill was installed
|
||||
# while the gateway was stopped (or after a gateway restart), the file watcher
|
||||
# won't detect the new SKILL.md and existing sessions will never refresh.
|
||||
#
|
||||
# This script clears stale snapshots so every session rebuilds on next turn.
|
||||
# This script:
|
||||
# 1. Copies the plugin to ~/.openclaw/extensions/memory-continuity/
|
||||
# 2. Adds the plugin entry to openclaw.json (if not present)
|
||||
# 3. Restarts the gateway
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/post-install.sh [--agent-id <id>]
|
||||
#
|
||||
# Options:
|
||||
# --agent-id <id> Agent ID (default: main)
|
||||
# bash scripts/post-install.sh
|
||||
#
|
||||
# Safe to run multiple times (idempotent).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AGENT_ID="main"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--agent-id) AGENT_ID="$2"; shift 2 ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
OPENCLAW_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
|
||||
SESSIONS_FILE="$OPENCLAW_DIR/agents/$AGENT_ID/sessions/sessions.json"
|
||||
EXTENSIONS_DIR="$OPENCLAW_DIR/extensions"
|
||||
PLUGIN_DIR="$EXTENSIONS_DIR/memory-continuity"
|
||||
CONFIG_FILE="$OPENCLAW_DIR/openclaw.json"
|
||||
|
||||
if [[ ! -f "$SESSIONS_FILE" ]]; then
|
||||
echo "No sessions file found at $SESSIONS_FILE — nothing to do."
|
||||
exit 0
|
||||
# Resolve the repo root (parent of scripts/)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
echo "=== memory-continuity plugin installer ==="
|
||||
echo ""
|
||||
|
||||
# Step 1: Copy plugin files to extensions directory
|
||||
echo "[1/3] Installing plugin to $PLUGIN_DIR ..."
|
||||
mkdir -p "$EXTENSIONS_DIR"
|
||||
|
||||
# Remove old installation if exists
|
||||
if [[ -d "$PLUGIN_DIR" || -L "$PLUGIN_DIR" ]]; then
|
||||
rm -rf "$PLUGIN_DIR"
|
||||
echo " Removed previous installation"
|
||||
fi
|
||||
|
||||
# Count and clear stale skillsSnapshot entries
|
||||
BEFORE=$(python3 -c "
|
||||
# Copy essential plugin files (not the entire repo)
|
||||
mkdir -p "$PLUGIN_DIR"
|
||||
for f in index.js openclaw.plugin.json SKILL.md; do
|
||||
if [[ -f "$REPO_DIR/$f" ]]; then
|
||||
cp "$REPO_DIR/$f" "$PLUGIN_DIR/"
|
||||
fi
|
||||
done
|
||||
echo " Copied plugin files"
|
||||
|
||||
# Step 2: Add plugin entry to openclaw.json
|
||||
echo "[2/3] Configuring openclaw.json ..."
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
echo " WARNING: $CONFIG_FILE not found. Skipping config update."
|
||||
echo " You'll need to manually add the plugin entry."
|
||||
else
|
||||
# Check if memory-continuity entry already exists
|
||||
if python3 -c "
|
||||
import json, sys
|
||||
with open('$SESSIONS_FILE') as f:
|
||||
with open('$CONFIG_FILE') as f:
|
||||
data = json.load(f)
|
||||
count = sum(1 for v in data.values() if isinstance(v, dict) and 'skillsSnapshot' in v)
|
||||
print(count)
|
||||
" 2>/dev/null || echo "0")
|
||||
|
||||
if [[ "$BEFORE" == "0" ]]; then
|
||||
echo "No stale skill snapshots found — all sessions will discover skills normally."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 -c "
|
||||
entries = data.get('plugins', {}).get('entries', {})
|
||||
if 'memory-continuity' in entries:
|
||||
print('exists')
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; then
|
||||
echo " Plugin entry already exists in config"
|
||||
else
|
||||
# Add the plugin entry
|
||||
python3 -c "
|
||||
import json
|
||||
path = '$SESSIONS_FILE'
|
||||
path = '$CONFIG_FILE'
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
for key in data:
|
||||
if isinstance(data[key], dict) and 'skillsSnapshot' in data[key]:
|
||||
del data[key]['skillsSnapshot']
|
||||
if 'plugins' not in data:
|
||||
data['plugins'] = {}
|
||||
if 'entries' not in data['plugins']:
|
||||
data['plugins']['entries'] = {}
|
||||
data['plugins']['entries']['memory-continuity'] = {
|
||||
'enabled': True,
|
||||
'hooks': {
|
||||
'allowPromptInjection': True
|
||||
},
|
||||
'config': {
|
||||
'maxStateLines': 50,
|
||||
'archiveOnNew': True,
|
||||
'autoExtract': True
|
||||
}
|
||||
}
|
||||
with open(path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
"
|
||||
|
||||
echo "Cleared skillsSnapshot from $BEFORE session(s) in $SESSIONS_FILE"
|
||||
echo "All sessions will rebuild their skill list on next turn."
|
||||
|
||||
# Restart gateway if running, so file watcher picks up the new SKILL.md
|
||||
if command -v openclaw &>/dev/null; then
|
||||
if openclaw gateway status 2>&1 | grep -q "running"; then
|
||||
echo "Restarting gateway to activate file watcher..."
|
||||
openclaw gateway restart 2>/dev/null && echo "Gateway restarted." || echo "Gateway restart failed — try manually: openclaw gateway restart"
|
||||
print(' Added plugin entry to config')
|
||||
" 2>/dev/null || echo " WARNING: Could not update config automatically. Add manually."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 3: Restart gateway
|
||||
echo "[3/3] Restarting gateway ..."
|
||||
if command -v openclaw &>/dev/null; then
|
||||
if openclaw gateway status 2>&1 | grep -q "running"; then
|
||||
openclaw gateway restart 2>/dev/null && echo " Gateway restarted" || echo " Gateway restart failed — try: openclaw gateway restart"
|
||||
else
|
||||
echo " Gateway not running — start it with: openclaw gateway start"
|
||||
fi
|
||||
else
|
||||
echo " openclaw command not found — install OpenClaw first"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Installation complete ==="
|
||||
echo ""
|
||||
echo "Verify with:"
|
||||
echo " openclaw gateway restart 2>&1 | grep memory-continuity"
|
||||
echo ""
|
||||
echo "Test with:"
|
||||
echo " 1. Tell your agent something memorable"
|
||||
echo " 2. Send /new"
|
||||
echo " 3. Ask what you told it"
|
||||
|
||||
Reference in New Issue
Block a user