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:
2026-03-15 14:11:14 +10:00
co-authored by Claude Opus 4.6
parent 38c1fe875d
commit 9ab2667aa3
4 changed files with 495 additions and 190 deletions
+100 -141
View File
@@ -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
OpenClaws 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
+269
View File
@@ -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;
+32
View File
@@ -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" }
}
}
+90 -45
View File
@@ -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
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)
"
print(' Added plugin entry to config')
" 2>/dev/null || echo " WARNING: Could not update config automatically. Add manually."
fi
fi
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
# 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
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"
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"