12 Commits
Author SHA1 Message Date
taodengandClaude Opus 4.6 9b1b99ea21 release: v2.3.0 — first fully stable release
Bump version to 2.3.0 across package.json, openclaw.plugin.json.
Add comprehensive CHANGELOG documenting all bugs found and fixed
during the v2.x development cycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 17:33:22 +10:00
taodengandClaude Opus 4.6 24221ea749 fix: prevent trivial conversations from overwriting meaningful state
Short conversations (< 2 real user messages) no longer overwrite
existing meaningful state. This prevents the "recovery death spiral"
where:
1. Secret told → state saved 
2. /new → state injected → model ignores it → "I don't remember"
3. agent_end overwrites good state with "I don't remember" 
4. All subsequent recoveries fail permanently

Now step 3 is blocked: a 1-message "what was my secret?" conversation
won't overwrite the previously saved secret.

Also strips channel metadata and /new-style short commands from
the real message count.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 17:25:04 +10:00
taodengandClaude Opus 4.6 2fc42661cf fix: always update state on agent_end, archive previous state
The agent_end hook was skipping state extraction when CURRENT_STATE.md
already contained meaningful content. This meant once a state was written,
subsequent conversations never updated it — the next /new would always
recover stale data.

Now: every session end overwrites CURRENT_STATE.md with the latest
conversation state. Previous state is archived to STATE_ARCHIVE_<ts>.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 17:10:29 +10:00
taodengandClaude Opus 4.6 ddc472b63f chore: bump version to 2.2.0
v2.2.0 — first fully working plugin release:
- Fix: resolve workspace dir from hook context (_ctx), not api.runtime
- Fix: strip channel metadata from user messages before state extraction
- Both fixes are required for gateway/Telegram/Discord deployments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 15:05:56 +10:00
taodengandClaude Opus 4.6 53c75986fa fix: use _ctx.workspaceDir and strip channel metadata
- Hook callbacks now get workspace from _ctx.workspaceDir (the hook
  context parameter) instead of api.runtime.workspaceDir which is
  undefined in gateway mode. This was the root cause of the plugin
  silently doing nothing on all remote/gateway deployments.

- extractStateFromMessages now strips Telegram/Discord channel
  metadata ("Conversation info (untrusted metadata)") from user
  messages before saving to CURRENT_STATE.md, preventing garbage
  data in the Objective field.

Tested on: macOS local (GPT-5.4), Oracle Cloud (MiniMax M2.5)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 14:44:12 +10:00
taodengandClaude Opus 4.6 86d820af81 fix: add install record for provenance tracking
post-install.sh now writes plugins.installs with source:"path" so
OpenClaw recognizes the plugin origin. Eliminates both the allow-list
and provenance warnings on fresh install. Script is now fully
idempotent — always refreshes allow/installs/entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 13:59:26 +10:00
taodengandClaude Opus 4.6 7d7e0e7bdb v2.1.0: fix provenance warnings, clean install experience
Add package.json with openclaw plugin metadata, configure plugins.allow
in post-install.sh, and add source field to plugin manifest. Fresh
installs now load without trust/provenance warnings in gateway logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 13:51:19 +10:00
taodengandClaude Opus 4.6 61f2d34366 docs: add "Why this plugin?" section highlighting differentiators
Emphasize zero-dependency, plain-file, backup/migrate simplicity,
upgrade-proof design vs vector-DB-based memory plugins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:18:06 +10:00
taodengandClaude Opus 4.6 7088d3d7d0 docs: update changelog and GitHub description for v2.0.0
Unify version references across all files. Update GitHub repo
description from "Draft skill" to "Lifecycle plugin".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:13:50 +10:00
taodengandClaude Opus 4.6 9ab2667aa3 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>
2026-03-15 14:11:14 +10:00
taodengandClaude Opus 4.6 38c1fe875d feat: add post-install script and troubleshooting guide
Add scripts/post-install.sh to clear stale skillsSnapshot cache from
existing sessions, ensuring the skill loads immediately after install.
Update README with corrected install path and post-install instructions.
Include detailed troubleshooting report documenting the snapshot cache
root cause analysis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:36:12 +10:00
taodeng dcc9093182 docs: clarify discord continuity is unsupported in alpha 2026-03-13 10:05:32 +10:00
7 changed files with 824 additions and 139 deletions
+63 -18
View File
@@ -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.
+118 -121
View File
@@ -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
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/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
+173
View File
@@ -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` — 显示 readysource 为 `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()
→ 优先使用 skillsSnapshotsession 缓存)
→ 否则调用 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` | 新建 |
+303
View File
@@ -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;
+33
View File
@@ -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" }
}
}
+19
View File
@@ -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"
}
}
+115
View File
@@ -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"