mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-21 21:15:07 +00:00
Compare commits
12
Commits
v0.3.0-probe
...
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b1b99ea21 | ||
|
|
24221ea749 | ||
|
|
2fc42661cf | ||
|
|
ddc472b63f | ||
|
|
53c75986fa | ||
|
|
86d820af81 | ||
|
|
7d7e0e7bdb | ||
|
|
61f2d34366 | ||
|
|
7088d3d7d0 | ||
|
|
9ab2667aa3 | ||
|
|
38c1fe875d | ||
|
|
dcc9093182 |
+63
-18
@@ -1,28 +1,73 @@
|
||||
# Changelog
|
||||
|
||||
## v0.3.0-probe — 2026-03-13
|
||||
## v2.3.0 — 2026-03-16
|
||||
|
||||
### Summary
|
||||
This release marks the transition from a skill-only continuity package to a
|
||||
**dual-form package**:
|
||||
- the existing `SKILL.md` remains the fallback behavior contract
|
||||
- a new **lifecycle plugin probe** is included to validate the primary runtime path
|
||||
First **fully stable** release. All previous versions had critical bugs causing silent failure on gateway deployments.
|
||||
|
||||
### Fixed
|
||||
- **Workspace resolution** (`_ctx.workspaceDir`) — Hooks now read workspace path from the hook context parameter instead of `api.runtime.workspaceDir` (which was always `undefined` in gateway mode). This was the root cause of the plugin silently doing nothing on all gateway/Telegram/Discord deployments.
|
||||
- **Channel metadata stripping** — User messages from Telegram/Discord are cleaned of `Conversation info (untrusted metadata)` prefixes before saving, preventing garbage in the state file.
|
||||
- **Recovery death spiral** — Short conversations (< 2 real user messages) no longer overwrite existing meaningful state. Previously: tell secret → `/new` → model ignores injected context → "I don't remember" → `agent_end` overwrites secret with failure → permanent data loss.
|
||||
- **State staleness** — Removed "skip if existing state is meaningful" guard that prevented state from ever updating after the first write.
|
||||
|
||||
### Debugging timeline
|
||||
1. Hooks not firing → `api.runtime.workspaceDir` was `undefined` → fix: use `_ctx.workspaceDir`
|
||||
2. State file full of Telegram metadata → fix: strip `Conversation info` / `Sender` prefixes
|
||||
3. State never updating → "Existing state is meaningful, skipping" → fix: always update
|
||||
4. Good state overwritten by bad → fix: require ≥ 2 real user messages to overwrite
|
||||
|
||||
### Tested on
|
||||
- macOS: GPT-5.4 (openai-codex) ✅
|
||||
- Oracle Cloud Linux: MiniMax M2.5 ✅
|
||||
- Multiple OpenClaw agents via Telegram ✅
|
||||
|
||||
### Known limitations
|
||||
- State extraction uses last user/assistant message only (not full summary)
|
||||
- Models that ignore `prependSystemContext` won't surface recovered state (state file is preserved, not overwritten). Adding recovery instructions to BOOTSTRAP.md helps.
|
||||
|
||||
---
|
||||
|
||||
## v2.2.0 — 2026-03-16
|
||||
|
||||
### Added
|
||||
- `plugin/lifecycle-prototype.ts`
|
||||
- `references/phase2-hook-validation.md`
|
||||
- `references/scope.md`
|
||||
- `references/plugin-design.md`
|
||||
- `package.json` with npm-style metadata and `openclaw.type: "plugin"` provenance
|
||||
- `plugins.allow` auto-configuration in `post-install.sh`
|
||||
- GitHub release with comparison table vs vector-DB plugins
|
||||
|
||||
### Fixed
|
||||
- "plugins.allow is empty" gateway warning
|
||||
- "loaded without install/load-path provenance" gateway warning
|
||||
|
||||
---
|
||||
|
||||
## v2.1.0 — 2026-03-16
|
||||
|
||||
### Added
|
||||
- `openclaw.plugin.json` manifest with `configSchema`
|
||||
- `scripts/post-install.sh` one-command installer
|
||||
|
||||
### Changed
|
||||
- repository direction clarified: primary long-term path is now a standard lifecycle plugin
|
||||
- ContextEngine is now documented as a future option, not the v1 default
|
||||
- README updated to describe the package as skill + lifecycle plugin probe
|
||||
- skill docs aligned to the lifecycle-plugin plan
|
||||
- README updated with "Why this plugin?" section
|
||||
|
||||
### Validated
|
||||
- Experiment A passed on multiple resident subagents (`tech_geek`, `travel_assistant` after workspace/startup-rule cleanup)
|
||||
- startup continuity injection can work without `read`
|
||||
---
|
||||
|
||||
### Pending
|
||||
- Experiment C (compaction-path verification) remains pending because no real compaction event was triggered in the earlier pressure test
|
||||
## v2.0.0 — 2026-03-15
|
||||
|
||||
### Summary
|
||||
Architecture upgrade: skill (prompt-based) → **plugin** (lifecycle hooks).
|
||||
|
||||
### Added
|
||||
- `index.js` with 5 lifecycle hooks: `before_agent_start`, `before_compaction`, `before_reset`, `agent_end`, `session_end`
|
||||
- Automated state injection via `prependSystemContext`
|
||||
- Auto-extraction of working state from conversation
|
||||
|
||||
### Removed
|
||||
- Dependency on model cooperation for state read/write
|
||||
|
||||
---
|
||||
|
||||
## v1.0.0 — 2026-03-14
|
||||
|
||||
### Summary
|
||||
Initial release as a skill (SKILL.md only). Required model to voluntarily read/write `CURRENT_STATE.md`. Did not work reliably with weaker models.
|
||||
|
||||
@@ -1,92 +1,123 @@
|
||||
# memory-continuity
|
||||
|
||||
**Current release:** `v0.3.0-probe`
|
||||
**Current release:** `v2.1.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
|
||||
OpenClaw **lifecycle plugin** for short-term working continuity. Preserves structured in-flight work state across `/new`, reset, gateway restarts, model fallback, and context compaction.
|
||||
|
||||
Its goal is to let an agent recover structured in-flight work state after `/new`, reset, gateway interruption, model fallback, or compaction.
|
||||
## Why this plugin?
|
||||
|
||||
There are feature-rich memory plugins out there (vector search, semantic dedup, smart extraction). We took a different path:
|
||||
|
||||
- **Zero dependency** — no embedding API, no vector DB, no external services
|
||||
- **Plain files** — data is markdown, human-readable, editable, greppable
|
||||
- **Hook-driven** — doesn't rely on model behavior, works with any model
|
||||
- **Backup = copy** — `cp` / `scp` / `rsync` is your entire backup strategy
|
||||
- **Migrate in seconds** — copy files to new host, done. No re-indexing, no model binding
|
||||
- **Upgrade-proof** — doesn't occupy the `contextEngine` slot, doesn't depend on OpenClaw internals
|
||||
- **Native-consistent** — aligns with OpenClaw's `memory/` file conventions
|
||||
|
||||
If what you need is "don't lose work across sessions" rather than "semantic search over 100k memories", this plugin is for you.
|
||||
|
||||
## 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
|
||||
|
||||
This repository should now be understood as a **continuity package**, not just a standalone skill.
|
||||
The plugin uses OpenClaw lifecycle hooks to **automatically** save and restore working state — no model cooperation needed.
|
||||
|
||||
### Included forms
|
||||
- **Skill** = behavior contract / fallback implementation / human-readable protocol
|
||||
- **Lifecycle plugin probe** = current runtime experiment for the primary architecture
|
||||
| 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 |
|
||||
|
||||
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/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. Add `memory-continuity` to `plugins.allow` (trust list)
|
||||
4. Add install record to `plugins.installs` (provenance tracking)
|
||||
5. Restart the gateway
|
||||
|
||||
No npm install, no API keys, no external database.
|
||||
|
||||
### 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": {
|
||||
"allow": ["memory-continuity"],
|
||||
"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
|
||||
@@ -96,7 +127,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
|
||||
@@ -108,100 +138,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
|
||||
├── 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/
|
||||
└── 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,173 @@
|
||||
# memory-continuity Skill 排错报告
|
||||
|
||||
> 日期: 2026-03-14
|
||||
> 排错人: Claude Opus 4.6 (via Claude Code)
|
||||
> 环境: OpenClaw 2026.3.12 (6472949) / macOS / Node 25.8.0
|
||||
|
||||
---
|
||||
|
||||
## 问题描述
|
||||
|
||||
memory-continuity skill 安装后无法正常工作。具体表现:
|
||||
|
||||
1. `/new` 重置会话后,agent 不按照 skill 定义的恢复优先级协议行事
|
||||
2. Agent 说"我不记得",然后才提到 `CURRENT_STATE.md` 中的"残留记录"
|
||||
3. `openclaw skills check` 显示 skill 状态为 `✓ ready`,但 agent 的 system prompt 中没有加载它
|
||||
|
||||
---
|
||||
|
||||
## 排错过程
|
||||
|
||||
### 第一阶段:确认 skill 文件完整性
|
||||
|
||||
- 检查 `~/.openclaw/workspace/main/skills/memory-continuity/SKILL.md` — 存在且 frontmatter 格式正确
|
||||
- 检查 `openclaw.json` — 5 个 agent 的 `skills` 数组中均已包含 `"memory-continuity"`
|
||||
- 检查 `openclaw skills check` — 显示 ready,source 为 `openclaw-workspace`
|
||||
- **结论:安装和配置层面无问题**
|
||||
|
||||
### 第二阶段:验证 skill 是否进入 agent prompt
|
||||
|
||||
通过 `openclaw agent --agent main -m "ping" --json` 获取 `systemPromptReport`,发现:
|
||||
|
||||
- 加载了 20 个 skills,**memory-continuity 不在其中**
|
||||
- 对比发现 `secureclaw`(同为 workspace skill)成功加载
|
||||
|
||||
对比两者差异:
|
||||
| 项目 | secureclaw | memory-continuity |
|
||||
|---|---|---|
|
||||
| SKILL.md | ✓ | ✓ |
|
||||
| skill.json | ✓ | ✗ |
|
||||
| _meta.json | ✓ | ✗ |
|
||||
|
||||
尝试为 memory-continuity 补充 `skill.json` 和 `_meta.json` 后重启 gateway,**问题未解决**。
|
||||
|
||||
### 第三阶段:逆向分析 skill loader 源码
|
||||
|
||||
反编译分析 OpenClaw 的 skill 加载链路:
|
||||
|
||||
```
|
||||
resolveSkillsPromptForRun()
|
||||
→ 优先使用 skillsSnapshot(session 缓存)
|
||||
→ 否则调用 buildWorkspaceSkillSnapshot()
|
||||
→ resolveWorkspaceSkillPromptState()
|
||||
→ filterSkillEntries()
|
||||
→ shouldIncludeSkill() // 过滤
|
||||
→ skillFilter // allowlist
|
||||
```
|
||||
|
||||
关键发现:
|
||||
|
||||
1. **skill 发现机制**(`loadSkillEntries`)基于文件系统扫描 `workspace/skills/*/SKILL.md`,与 `_meta.json` 和 `skill.json` 无关
|
||||
2. **skill 过滤机制**(`filterSkillEntries`)使用 agent 配置中的 `skills` 数组作为 allowlist
|
||||
3. **skill snapshot 缓存**(`skillsSnapshot`)存储在 session store 中,只在以下条件刷新:
|
||||
- `isFirstTurnInSession`(首轮对话)
|
||||
- `snapshotVersion > 0` 且版本号增加
|
||||
|
||||
### 第四阶段:定位根因
|
||||
|
||||
检查 session store 中的 skills snapshot:
|
||||
|
||||
```json
|
||||
// ~/.openclaw/agents/main/sessions/sessions.json
|
||||
// session "agent:main:main"
|
||||
{
|
||||
"skillsSnapshot": {
|
||||
"version": 0,
|
||||
"skills": [/* 20 个 skill,不含 memory-continuity */]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**根因确认:**
|
||||
|
||||
- 所有 239 个 session 在 memory-continuity 安装前就已缓存了 skills snapshot
|
||||
- snapshot `version: 0`,而刷新条件是 `snapshotVersion > 0`,导致永远不会自动刷新
|
||||
- 后续对话复用已有 session(非 firstTurn),跳过重建
|
||||
- 结果:无论怎么重启 gateway 或重装 skill,缓存的旧 snapshot 始终被使用
|
||||
|
||||
---
|
||||
|
||||
## 修复措施
|
||||
|
||||
### 1. 清除所有 session 的过期 skillsSnapshot(关键修复)
|
||||
|
||||
```python
|
||||
# 遍历 sessions.json,删除所有 session 的 skillsSnapshot 字段
|
||||
for key in data:
|
||||
if 'skillsSnapshot' in data[key]:
|
||||
del data[key]['skillsSnapshot']
|
||||
# 共清除 239 个 session 的缓存
|
||||
```
|
||||
|
||||
下次 agent 响应时,检测到 `!current.skillsSnapshot`,触发 `buildWorkspaceSkillSnapshot()` 重建,新 snapshot 包含 memory-continuity。
|
||||
|
||||
### 2. 补充 skill.json(规范性改进)
|
||||
|
||||
创建 `workspace/main/skills/memory-continuity/skill.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "memory-continuity",
|
||||
"version": "1.0.0",
|
||||
"description": "Short-term working continuity for OpenClaw agents...",
|
||||
"author": "dtzp555-max",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/dtzp555-max/memory-continuity"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 补充 _meta.json(规范性改进)
|
||||
|
||||
创建 `workspace/main/skills/memory-continuity/_meta.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ownerId": "github:dtzp555-max",
|
||||
"slug": "memory-continuity",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1710388800000
|
||||
}
|
||||
```
|
||||
|
||||
> 注:措施 2 和 3 对 skill 加载无实际影响,但与其他 workspace skill(如 secureclaw)保持一致。
|
||||
|
||||
---
|
||||
|
||||
## 验证结果
|
||||
|
||||
### Skill 加载验证
|
||||
|
||||
```
|
||||
修复前: Total: 20, memory-continuity: False
|
||||
修复后: Total: 11, memory-continuity: True (620 chars)
|
||||
```
|
||||
|
||||
加载的 11 个 skills 与 main agent 配置的 `skills` 数组完全匹配。
|
||||
|
||||
### 功能黑盒测试
|
||||
|
||||
| 步骤 | 操作 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 告诉 agent 秘密信息 | agent 确认记录 |
|
||||
| 2 | 检查 CURRENT_STATE.md | 秘密已写入 `## In Flight` |
|
||||
| 3 | 删除 session 模拟 `/new` | 新 session 创建 |
|
||||
| 4 | 在新 session 中问秘密 | agent 从 CURRENT_STATE.md 恢复,正确回答 |
|
||||
|
||||
---
|
||||
|
||||
## 经验总结
|
||||
|
||||
1. **OpenClaw 的 skill 加载不是实时的** — session store 中的 `skillsSnapshot` 会缓存 skill 列表,新安装的 skill 不会自动出现在已有 session 中
|
||||
2. **`openclaw skills check` 显示 ready 不代表已加载** — ready 只表示文件系统发现成功,实际加载还受 session snapshot 缓存影响
|
||||
3. **snapshot version = 0 是一个 edge case** — 在这个版本下,自动刷新逻辑永远不会触发(`snapshotVersion > 0` 为 false)
|
||||
4. **重启 gateway 不会清除 session snapshot** — snapshot 持久化在 sessions.json 中,只有清除缓存或触发 firstTurn 才能刷新
|
||||
|
||||
---
|
||||
|
||||
## 涉及文件
|
||||
|
||||
| 文件 | 操作 |
|
||||
|---|---|
|
||||
| `~/.openclaw/agents/main/sessions/sessions.json` | 清除 239 个 session 的 skillsSnapshot |
|
||||
| `~/.openclaw/workspace/main/skills/memory-continuity/skill.json` | 新建 |
|
||||
| `~/.openclaw/workspace/main/skills/memory-continuity/_meta.json` | 新建 |
|
||||
@@ -0,0 +1,303 @@
|
||||
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;
|
||||
// Strip channel metadata (Telegram, Discord, etc.) from user messages
|
||||
const cleaned = role === "user"
|
||||
? content
|
||||
.replace(/^Conversation info \(untrusted metadata\):[\s\S]*?\n\n/m, "")
|
||||
.replace(/^Sender \(untrusted metadata\):[\s\S]*?\n\n/m, "")
|
||||
.trim()
|
||||
: content;
|
||||
if (role === "user" && cleaned) userMessages.push(cleaned);
|
||||
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 getConfig = () => api.pluginConfig || {};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HOOK 1: before_agent_start — inject recovered state into context
|
||||
// ------------------------------------------------------------------
|
||||
api.on("before_agent_start", async (_event, _ctx) => {
|
||||
const ws = _ctx?.workspaceDir;
|
||||
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 = _ctx?.workspaceDir;
|
||||
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 = _ctx?.workspaceDir;
|
||||
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 = _ctx?.workspaceDir;
|
||||
const config = getConfig();
|
||||
if (!ws || config.autoExtract === false) return;
|
||||
|
||||
const statePath = resolveStatePath(ws);
|
||||
if (!statePath) return;
|
||||
|
||||
const messages = event?.messages;
|
||||
if (!messages || messages.length === 0) {
|
||||
log.info?.("[memory-continuity] No messages in session, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Count real user messages (exclude system, metadata-only, short commands)
|
||||
const realUserMsgs = messages.filter(m => {
|
||||
if (m?.role !== "user") return false;
|
||||
const text = typeof m?.content === "string" ? m.content
|
||||
: Array.isArray(m?.content) ? m.content.filter(b => b?.type === "text").map(b => b.text).join("\n")
|
||||
: "";
|
||||
const cleaned = text
|
||||
.replace(/^Conversation info \(untrusted metadata\):[\s\S]*?\n\n/m, "")
|
||||
.replace(/^Sender \(untrusted metadata\):[\s\S]*?\n\n/m, "")
|
||||
.trim();
|
||||
// Skip very short messages like "/new", "/status", single-word queries
|
||||
return cleaned.length > 10;
|
||||
});
|
||||
|
||||
// Don't overwrite meaningful state with trivial conversations
|
||||
// (e.g., "/new" then "what was my secret?" — only 1 real message)
|
||||
const existing = readFile(statePath);
|
||||
if (existing && buildSnapshot(existing) && realUserMsgs.length < 2) {
|
||||
log.info?.("[memory-continuity] Conversation too short to overwrite existing state (" + realUserMsgs.length + " real msgs)");
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = extractStateFromMessages(messages);
|
||||
if (!newState) {
|
||||
log.info?.("[memory-continuity] No extractable state from conversation");
|
||||
return;
|
||||
}
|
||||
|
||||
// Archive previous state if it exists
|
||||
if (existing) {
|
||||
const archivePath = statePath.replace(/CURRENT_STATE\.md$/, `STATE_ARCHIVE_${Date.now()}.md`);
|
||||
writeFile(archivePath, existing);
|
||||
}
|
||||
|
||||
writeFile(statePath, newState);
|
||||
log.info?.("[memory-continuity] Updated state from conversation (" + realUserMsgs.length + " real msgs)");
|
||||
}, { 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 = _ctx?.workspaceDir;
|
||||
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,33 @@
|
||||
{
|
||||
"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.3.0",
|
||||
"source": "https://github.com/dtzp555-max/memory-continuity",
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "memory-continuity",
|
||||
"version": "2.3.0",
|
||||
"description": "Zero-dependency memory continuity for OpenClaw — plain markdown, lifecycle hooks, no vector DB.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dtzp555-max/memory-continuity.git"
|
||||
},
|
||||
"keywords": ["openclaw", "plugin", "memory", "continuity"],
|
||||
"author": "dtzp555-max",
|
||||
"license": "MIT",
|
||||
"openclaw": {
|
||||
"type": "plugin",
|
||||
"id": "memory-continuity",
|
||||
"pluginManifest": "openclaw.plugin.json"
|
||||
}
|
||||
}
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
# post-install.sh — Install memory-continuity as an OpenClaw lifecycle plugin
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# Safe to run multiple times (idempotent).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OPENCLAW_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
|
||||
EXTENSIONS_DIR="$OPENCLAW_DIR/extensions"
|
||||
PLUGIN_DIR="$EXTENSIONS_DIR/memory-continuity"
|
||||
CONFIG_FILE="$OPENCLAW_DIR/openclaw.json"
|
||||
|
||||
# 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
|
||||
|
||||
# Copy essential plugin files (not the entire repo)
|
||||
mkdir -p "$PLUGIN_DIR"
|
||||
for f in index.js openclaw.plugin.json package.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
|
||||
# Always ensure allow list, install record, and entry are present (idempotent)
|
||||
python3 -c "
|
||||
import json
|
||||
path = '$CONFIG_FILE'
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
if 'plugins' not in data:
|
||||
data['plugins'] = {}
|
||||
# Add to plugins.allow so OpenClaw trusts this plugin (no provenance warning)
|
||||
allow_list = data['plugins'].get('allow', [])
|
||||
if 'memory-continuity' not in allow_list:
|
||||
allow_list.append('memory-continuity')
|
||||
data['plugins']['allow'] = allow_list
|
||||
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
|
||||
}
|
||||
}
|
||||
# Add install record for provenance tracking (eliminates untracked-code warning)
|
||||
if 'installs' not in data['plugins']:
|
||||
data['plugins']['installs'] = {}
|
||||
data['plugins']['installs']['memory-continuity'] = {
|
||||
'source': 'path',
|
||||
'installPath': '~/.openclaw/extensions/memory-continuity',
|
||||
'sourcePath': '$REPO_DIR'
|
||||
}
|
||||
with open(path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
print(' Added plugin entry, trust config, and install record')
|
||||
" 2>/dev/null || echo " WARNING: Could not update config automatically. Add manually."
|
||||
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