27 Commits
Author SHA1 Message Date
taodengandClaude Opus 4.6 fb0fe6b40b feat: add archive count limits and unify versions (v2.7.0)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:36:55 +10:00
taodengandClaude Sonnet 4.6 2184442bc2 fix: correct openclaw.json key paths in verify.sh --all-agents
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 10:52:39 +10:00
taodengandClaude Sonnet 4.6 719723225d feat: add interactive setup wizard with model selection and agent config (v2.6.0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 10:34:39 +10:00
taodengandClaude Sonnet 4.6 9f43452f03 fix: resolve workspace path from openclaw.json for cross-machine compatibility
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 09:28:53 +10:00
taodeng fe13ea19be fix: resolve workspace path from openclaw.json for cross-machine compatibility (v2.5.0) 2026-03-19 09:28:20 +10:00
taodeng 772c964740 fix: increase agent selection timeout from 10s to 20s, bump v2.4.2 2026-03-19 09:25:18 +10:00
taodeng b12bd15c8b fix: bash case-upper compat (tr instead of ^^) 2026-03-19 09:24:19 +10:00
taodengandClaude Sonnet 4.6 c366d3ec47 fix: correct agents.list path in openclaw.json detection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 09:23:42 +10:00
taodengandClaude Opus 4.6 449f1f4916 feat: detect only alive agents during install, auto-default to All with 10s timeout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 09:13:40 +10:00
taodengandClaude Sonnet 4.6 6a60c1806d feat: agent detection + selective install (v2.4.0)
- post-install.sh: reads openclaw.json `list` array via python3,
  presents a numbered menu (all/specific/quit), installs SKILL.md
  to <workspace>/skills/memory-continuity/SKILL.md for chosen agents;
  falls back to manual path prompt if config absent
- post-install.sh: remove gateway restart step
- verify.sh: add --all-agents flag that runs the 3-layer check
  (SKILL.md, continuity_doctor.py, CURRENT_STATE.md) across every
  detected agent workspace and reports pass/fail per agent
- openclaw.plugin.json: bump version to 2.4.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 07:26:00 +10:00
taodeng 9e25ec5e95 fix: python 3.6 compat + clarify CURRENT_STATE.md override rule 2026-03-19 07:14:41 +10:00
taodengandClaude Sonnet 4.6 312d892853 fix: Python 3.6 compat + allow CURRENT_STATE.md override
- Replace 'Path | None' union syntax with 'Optional[Path]' (typing import)
  so continuity_doctor.py works on Python 3.6+
- Remove message-count guard in agent_end hook that blocked state writes
  when realUserMsgs.length < 2; agents can now update CURRENT_STATE.md
  regardless of conversation length

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 07:13:26 +10:00
taodengandClaude Sonnet 4.6 33c3558625 feat: add scripts/verify.sh with 3-layer validation
Layer 1: checks SKILL.md, continuity_doctor.py, openclaw.plugin.json exist
Layer 2: runs continuity_doctor.py to confirm tool is available
Layer 3: detects empty/placeholder CURRENT_STATE.md and suggests --sample

--sample flag shows a realistic production migration task (db v2→v3,
blocked on migration window sign-off) so new users understand what
high-importance state looks like before their first session.

Colour output with  ⚠️ . README Quick Start updated with verify commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 06:53:02 +10:00
taodengandClaude Opus 4.6 201c52bf75 docs: translate recovery section to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 06:22:50 +10:00
taodengandClaude Opus 4.6 d61c242e9f docs: add upgrade recovery guide and troubleshooting table
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 06:05:10 +10:00
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
11 changed files with 2116 additions and 142 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.
+173 -121
View File
@@ -1,92 +1,136 @@
# memory-continuity
**Current release:** `v0.3.0-probe`
**Current release:** `v2.7.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
# Quick 3-layer install check (files → tool → workspace state)
bash scripts/verify.sh
# Show a sample high-importance state entry
bash scripts/verify.sh --sample
# Check against a custom workspace
bash scripts/verify.sh --workspace ~/.openclaw/workspace/myproject
```
## How the current skill version works
```bash
# Alternatively, confirm the gateway loaded the plugin
openclaw gateway restart 2>&1 | grep memory-continuity
# Should show: [memory-continuity] Plugin registered successfully
```
The skill defines a discipline around one file:
- `memory/CURRENT_STATE.md`
### Test
That file is the short-term workbench for active work. It is:
- overwritten, not appended
- intentionally short
- structured for fast recovery
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
### The checkpoint shape
## Configuration
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 |
| `maxArchiveCount` | `20` | Maximum archive files to keep (oldest auto-deleted) |
## 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 +140,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 +151,109 @@ 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`
## Recovery after OpenClaw upgrade
### Important boundary
Session memory search is useful for:
- “what did we discuss before?”
- “what decision was mentioned in a prior session?”
OpenClaw upgrades (`npm update -g openclaw`) **do not overwrite** the user config at `~/.openclaw/openclaw.json`. However, if the plugin stops working after an upgrade, follow these steps:
Memory continuity is for:
- “what are we doing right now?”
- “where did we stop?”
- “what should happen next?”
### Quick diagnosis
```bash
# Check if the plugin is loaded
openclaw gateway restart 2>&1 | grep memory-continuity
# Expected: [memory-continuity] Plugin registered successfully
# Verify config is intact
cat ~/.openclaw/openclaw.json | grep -A2 memory-continuity
```
### Common issues and fixes
| Symptom | Cause | Fix |
|---------|-------|-----|
| No `Plugin registered` in startup log | Plugin files missing or config lost | Re-run `bash scripts/post-install.sh` |
| `plugins.allow is empty` warning | `plugins.allow` was cleared in config | Add `"plugins.allow": ["memory-continuity"]` |
| `loaded without provenance` warning | `plugins.installs` record missing | Add `"plugins.installs": {"memory-continuity": {"source": "path"}}` |
| New version changed hook API | OpenClaw breaking change | Check [CHANGELOG](CHANGELOG.md), update `index.js` |
| State not recovering, no errors | Session cached stale skillsSnapshot | `/new` to start fresh session, or re-run `post-install.sh` (clears cache) |
### One-command recovery
The install script is idempotent — safe to re-run at any time:
```bash
cd ~/.openclaw/projects/memory-continuity # or wherever you cloned it
git pull # pull latest version
bash scripts/post-install.sh # reinstall + restart gateway
```
### Pre-upgrade backup (recommended)
```bash
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak
```
## Architecture: Plugin vs Skill
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.
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
├── verify.sh # 3-layer install verifier
└── 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
+16
View File
@@ -130,6 +130,22 @@ Update the file by **overwriting** it, not appending, at these moments:
| Before handoff / subagent exit | Preserves outputs and unsurfaced results |
| After a substantive state change | Keeps checkpoint aligned with actual work |
### Override rule
**CURRENT_STATE.md must always be overwritten when:**
- A new task or objective starts — regardless of what is currently in the file
- The previous objective is complete or abandoned
- The user gives a new task that supersedes the previous one
Having content in CURRENT_STATE.md does NOT mean it should be preserved.
Content only matters if Objective is still active and work is genuinely in progress.
Checking before overwrite:
- Read the file
- If Objective matches the current task → update in place (overwrite)
- If Objective does NOT match → overwrite the entire file with the new state
- Never append. Never skip the update because "there's already something there".
### 4. Keep the checkpoint small
`CURRENT_STATE.md` should usually stay under about 40 lines and be readable in
+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` | 新建 |
+343
View File
@@ -0,0 +1,343 @@
import fs from "node:fs";
import path from "node:path";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const MAX_ARCHIVE_COUNT = 20;
const MAX_MEMORY_FILES = 500;
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, config = {}) {
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");
// Enforce archive count limit
const maxCount = config.maxArchiveCount || MAX_ARCHIVE_COUNT;
const files = fs.readdirSync(archiveDir).sort();
if (files.length > maxCount) {
const toDelete = files.slice(0, files.length - maxCount);
for (const f of toDelete) {
try { fs.unlinkSync(path.join(archiveDir, f)); } catch {}
}
}
// Clean up memory directory
cleanupMemoryDir(workspaceDir);
}
function cleanupMemoryDir(workspaceDir) {
const memoryDir = path.join(workspaceDir, "memory");
try {
const entries = fs.readdirSync(memoryDir);
// Clean up legacy STATE_ARCHIVE_*.md files from memory/ root
const legacyArchives = entries.filter(f => /^STATE_ARCHIVE_.*\.md$/.test(f));
for (const f of legacyArchives) {
try { fs.unlinkSync(path.join(memoryDir, f)); } catch {}
}
// Check total file count (top level only)
const remaining = fs.readdirSync(memoryDir);
const fileEntries = remaining.filter(f => {
try { return fs.statSync(path.join(memoryDir, f)).isFile(); } catch { return false; }
});
if (fileEntries.length > MAX_MEMORY_FILES) {
const PROTECTED = new Set(["CURRENT_STATE.md", "MEMORY.md", "INDEX.md"]);
const deletable = fileEntries
.filter(f => !PROTECTED.has(f) && f.endsWith(".md"))
.map(f => ({ name: f, mtime: fs.statSync(path.join(memoryDir, f)).mtimeMs }))
.sort((a, b) => a.mtime - b.mtime);
const toRemove = deletable.slice(0, fileEntries.length - 450);
for (const { name } of toRemove) {
try { fs.unlinkSync(path.join(memoryDir, name)); } catch {}
}
}
} catch {}
}
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, config);
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;
});
const existing = readFile(statePath);
const newState = extractStateFromMessages(messages);
if (!newState) {
log.info?.("[memory-continuity] No extractable state from conversation");
return;
}
// Archive previous state if it exists
if (existing) {
archiveState(ws, existing, config);
}
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;
+51
View File
@@ -0,0 +1,51 @@
{
"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.7.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"
},
"maxArchiveCount": {
"type": "number",
"default": 20,
"description": "Maximum number of archive files to keep in session_archive/"
}
}
},
"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"
},
"maxArchiveCount": {
"label": "Max archive files",
"help": "Old archives are auto-deleted when this limit is reached"
}
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "memory-continuity",
"version": "2.7.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"
}
}
+4 -3
View File
@@ -15,6 +15,7 @@ import sys
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
@@ -32,7 +33,7 @@ class Severity:
# ---------------------------------------------------------------------------
class DiagnosticReport:
def __init__(self):
self.entries: list[tuple[str, str]] = []
self.entries: List[Tuple[str, str]] = []
self._worst = Severity.OK
def add(self, severity: str, message: str):
@@ -78,7 +79,7 @@ PLACEHOLDER_PATTERNS = [
# Checks
# ---------------------------------------------------------------------------
def check_existence(workspace: Path, report: DiagnosticReport) -> Path | None:
def check_existence(workspace: Path, report: DiagnosticReport) -> Optional[Path]:
"""Check that memory/CURRENT_STATE.md exists."""
state_file = workspace / "memory" / "CURRENT_STATE.md"
if not state_file.exists():
@@ -111,7 +112,7 @@ def check_staleness(state_file: Path, workspace: Path, report: DiagnosticReport)
def check_template_compliance(state_file: Path, report: DiagnosticReport) -> dict:
"""Check all required sections are present and not placeholder-only."""
content = state_file.read_text(encoding="utf-8")
sections_found: dict[str, str] = {}
sections_found: Dict[str, str] = {}
for section in REQUIRED_SECTIONS:
# Match ## Section or ## Section\n
+293
View File
@@ -0,0 +1,293 @@
#!/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. Detects OpenClaw agents and installs SKILL.md to selected workspaces
#
# 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
python3 -c "
import json
path = '$CONFIG_FILE'
with open(path) as f:
data = json.load(f)
if 'plugins' not in data:
data['plugins'] = {}
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
}
}
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: Detect agents and install SKILL.md to selected workspaces
# ---------------------------------------------------------------------------
echo "[3/3] Detecting OpenClaw agents ..."
GRAY='\033[0;90m'
RST='\033[0m'
# detect_agents outputs lines of: INDEX|ID|DISPLAY_NAME|WORKSPACE_PATH
# Uses python3 to parse openclaw.json; falls back gracefully if unavailable.
detect_agents() {
if [[ ! -f "$CONFIG_FILE" ]]; then
return 1
fi
python3 - "$CONFIG_FILE" "$OPENCLAW_DIR" <<'PYEOF'
import json, sys, os
config_file = sys.argv[1]
openclaw_dir = sys.argv[2]
with open(config_file) as f:
data = json.load(f)
default_ws = data.get('agents', {}).get('defaults', {}).get('workspace', os.path.join(openclaw_dir, 'workspace', 'main'))
agents = data.get('agents', {}).get('list', [])
seen = set()
idx = 1
for agent in agents:
agent_id = agent.get('id', '')
if not agent_id or agent_id in seen:
continue
seen.add(agent_id)
name = agent.get('name', agent_id)
workspace = agent.get('workspace', default_ws)
# Expand ~ in path
workspace = os.path.expanduser(workspace)
print('{}|{}|{}|{}'.format(idx, agent_id, name, workspace))
idx += 1
PYEOF
}
# Collect ALL detected agents (including non-alive) into arrays
ALL_AGENT_IDS=()
ALL_AGENT_NAMES=()
ALL_AGENT_WORKSPACES=()
if command -v python3 &>/dev/null && [[ -f "$CONFIG_FILE" ]]; then
while IFS='|' read -r idx agent_id agent_name workspace; do
ALL_AGENT_IDS+=("$agent_id")
ALL_AGENT_NAMES+=("$agent_name")
ALL_AGENT_WORKSPACES+=("$workspace")
done < <(detect_agents 2>/dev/null || true)
fi
# Filter to alive agents (workspace directory exists)
AGENT_IDS=()
AGENT_NAMES=()
AGENT_WORKSPACES=()
SKIPPED=0
for i in "${!ALL_AGENT_IDS[@]}"; do
if [[ -d "${ALL_AGENT_WORKSPACES[$i]}" ]]; then
AGENT_IDS+=("${ALL_AGENT_IDS[$i]}")
AGENT_NAMES+=("${ALL_AGENT_NAMES[$i]}")
AGENT_WORKSPACES+=("${ALL_AGENT_WORKSPACES[$i]}")
else
SKIPPED=$((SKIPPED + 1))
fi
done
install_skill_to_workspace() {
local workspace="$1"
local skill_dest="${workspace}/skills/memory-continuity"
mkdir -p "$skill_dest"
cp "$REPO_DIR/SKILL.md" "$skill_dest/SKILL.md"
echo " Installed → ${skill_dest}/SKILL.md"
}
if [[ ${#AGENT_IDS[@]} -eq 0 && ${#ALL_AGENT_IDS[@]} -gt 0 ]]; then
# Config has agents but none are alive
echo ""
echo " Found ${#ALL_AGENT_IDS[@]} agent(s) in config but their workspace directories don't exist yet."
echo " You may need to initialize them first."
echo ""
printf " Enter workspace path to install SKILL.md (or press Enter to skip): "
read -r FALLBACK_WS
if [[ -n "$FALLBACK_WS" ]]; then
FALLBACK_WS="${FALLBACK_WS/#\~/$HOME}"
install_skill_to_workspace "$FALLBACK_WS"
else
echo " Skipped SKILL.md installation."
fi
elif [[ ${#AGENT_IDS[@]} -eq 0 ]]; then
# No agents at all — ask user for a workspace path
echo " No agents detected in openclaw.json (file missing or parse error)."
echo ""
printf " Enter workspace path to install SKILL.md (or press Enter to skip): "
read -r FALLBACK_WS
if [[ -n "$FALLBACK_WS" ]]; then
FALLBACK_WS="${FALLBACK_WS/#\~/$HOME}"
install_skill_to_workspace "$FALLBACK_WS"
else
echo " Skipped SKILL.md installation."
fi
else
# Show numbered list — alive agents first, then skipped summary
echo ""
echo " Found ${#AGENT_IDS[@]} alive agent(s):"
for i in "${!AGENT_IDS[@]}"; do
num=$((i + 1))
printf " [%d] %s (%s)\n" "$num" "${AGENT_NAMES[$i]}" "${AGENT_WORKSPACES[$i]}"
done
# Show skipped (non-alive) agents in gray
if [[ $SKIPPED -gt 0 ]]; then
echo ""
printf " ${GRAY}Skipped %d agent(s) with missing workspace directories:${RST}\n" "$SKIPPED"
for i in "${!ALL_AGENT_IDS[@]}"; do
if [[ ! -d "${ALL_AGENT_WORKSPACES[$i]}" ]]; then
printf " ${GRAY} %s (%s)${RST}\n" "${ALL_AGENT_NAMES[$i]}" "${ALL_AGENT_WORKSPACES[$i]}"
fi
done
fi
echo ""
echo " Install to: [A]ll alive agents (default) / [1,2,...] specific / [Q]uit"
printf " > (20s timeout, Enter or no input = All) "
# Auto-select All if stdin is not a tty (pipe/CI) or on timeout
if [[ ! -t 0 ]]; then
SELECTION="A"
echo "(non-interactive: auto-selecting All)"
elif read -t 20 -r SELECTION; then
# User provided input (possibly empty = Enter)
: # SELECTION is set
else
# Timeout — default to All
echo ""
echo " (timeout — defaulting to All)"
SELECTION=""
fi
# Empty input (Enter or timeout) defaults to All
if [[ -z "$SELECTION" ]]; then
SELECTION="A"
fi
SELECTION_UPPER="$(echo "$SELECTION" | tr '[:lower:]' '[:upper:]')"
case "$SELECTION_UPPER" in
A|ALL)
for i in "${!AGENT_IDS[@]}"; do
install_skill_to_workspace "${AGENT_WORKSPACES[$i]}"
done
;;
Q|QUIT)
echo " Skipped SKILL.md installation."
;;
*)
# Parse comma-separated numbers
IFS=',' read -ra PICKS <<< "$SELECTION"
INSTALLED=0
for pick in "${PICKS[@]}"; do
pick="${pick// /}" # trim spaces
if [[ "$pick" =~ ^[0-9]+$ ]]; then
idx=$((pick - 1))
if [[ $idx -ge 0 && $idx -lt ${#AGENT_IDS[@]} ]]; then
install_skill_to_workspace "${AGENT_WORKSPACES[$idx]}"
INSTALLED=$((INSTALLED + 1))
else
echo " WARNING: No agent at index $pick — skipped."
fi
else
echo " WARNING: Invalid selection '$pick' — skipped."
fi
done
if [[ $INSTALLED -eq 0 ]]; then
echo " No valid selections — SKILL.md not installed."
fi
;;
esac
fi
echo ""
echo "🧙 启动配置向导..."
python3 "$(dirname "$0")/setup_wizard.py" || true
echo ""
echo "=== Installation complete ==="
echo ""
echo "Verify with:"
echo " bash scripts/verify.sh"
echo " bash scripts/verify.sh --all-agents"
echo ""
echo "Test with:"
echo " 1. Tell your agent something memorable"
echo " 2. Send /new"
echo " 3. Ask what you told it"
+600
View File
@@ -0,0 +1,600 @@
#!/usr/bin/env python3
# setup_wizard.py — Interactive post-install wizard for memory-continuity skill
#
# Guides the user through:
# Step 1/4: Select default model
# Step 2/4: Configure per-agent models
# Step 3/4: Review changes + OCP check
# Step 4/4: Restart gateway
#
# Usage:
# python3 scripts/setup_wizard.py
#
# Requires Python 3.6+
import sys
import os
import json
import subprocess
import termios
import tty
import signal
# ---------------------------------------------------------------------------
# Terminal colors (ANSI)
# ---------------------------------------------------------------------------
GREEN = "\033[32m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
RED = "\033[31m"
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"
def green(s): return f"{GREEN}{s}{RESET}"
def yellow(s): return f"{YELLOW}{s}{RESET}"
def cyan(s): return f"{CYAN}{s}{RESET}"
def red(s): return f"{RED}{s}{RESET}"
def bold(s): return f"{BOLD}{s}{RESET}"
def dim(s): return f"{DIM}{s}{RESET}"
# ---------------------------------------------------------------------------
# Key reading (raw terminal, no curses dependency)
# ---------------------------------------------------------------------------
UP_ARROW = "UP"
DOWN_ARROW = "DOWN"
ENTER_KEY = "ENTER"
CTRL_C = "CTRL_C"
def _read_key():
"""Read a single keypress from stdin. Returns a key name string."""
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
if ch == "\x03":
return CTRL_C
if ch in ("\r", "\n"):
return ENTER_KEY
if ch == "\x1b":
# Escape sequence — read more
ch2 = sys.stdin.read(1)
if ch2 == "[":
ch3 = sys.stdin.read(1)
if ch3 == "A":
return UP_ARROW
if ch3 == "B":
return DOWN_ARROW
return ch
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
# ---------------------------------------------------------------------------
# Clean exit on Ctrl+C anywhere
# ---------------------------------------------------------------------------
def _handle_sigint(sig, frame):
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
signal.signal(signal.SIGINT, _handle_sigint)
# ---------------------------------------------------------------------------
# Arrow-key menu selector
# ---------------------------------------------------------------------------
def arrow_select(prompt, options, default_index=0):
"""
Display a list of options with arrow-key navigation.
Returns the selected index, or raises SystemExit on Ctrl+C.
"""
selected = default_index
n = len(options)
# Hide cursor
sys.stdout.write("\033[?25l")
sys.stdout.flush()
def _render():
# Move cursor up to re-draw
sys.stdout.write(f"\033[{n}A\r")
for i, opt in enumerate(options):
if i == selected:
prefix = f" {CYAN}{BOLD}"
suffix = RESET
else:
prefix = " "
suffix = ""
sys.stdout.write(f"\r{prefix}{opt}{suffix}\n")
sys.stdout.flush()
# Initial render
if prompt:
print(prompt)
for i, opt in enumerate(options):
print(f" {opt}")
try:
while True:
_render()
key = _read_key()
if key == CTRL_C:
sys.stdout.write("\033[?25h")
sys.stdout.flush()
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
elif key == UP_ARROW:
selected = (selected - 1) % n
elif key == DOWN_ARROW:
selected = (selected + 1) % n
elif key == ENTER_KEY:
break
finally:
sys.stdout.write("\033[?25h")
sys.stdout.flush()
return selected
# ---------------------------------------------------------------------------
# Yes/No prompt
# ---------------------------------------------------------------------------
def yes_no(prompt, default_yes=True):
"""Ask a yes/no question. Returns True for yes."""
hint = "[Y/n]" if default_yes else "[y/N]"
while True:
try:
ans = input(f"{prompt} {hint}: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
if ans == "":
return default_yes
if ans in ("y", "yes"):
return True
if ans in ("n", "no"):
return False
print(yellow(" 请输入 Y 或 N"))
# ---------------------------------------------------------------------------
# Progress header
# ---------------------------------------------------------------------------
def print_step(n, total, title):
print()
print(bold(cyan(f"步骤 {n}/{total}: {title}")))
print(cyan("" * 56))
# ---------------------------------------------------------------------------
# Find openclaw.json
# ---------------------------------------------------------------------------
OPENCLAW_JSON_PATHS = [
os.path.expanduser("~/.openclaw/openclaw.json"),
"/etc/openclaw/openclaw.json",
]
def find_openclaw_json():
for p in OPENCLAW_JSON_PATHS:
if os.path.isfile(p):
return p
return None
def load_config(path):
with open(path, encoding="utf-8") as f:
return json.load(f)
def save_config(path, data):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
# ---------------------------------------------------------------------------
# Model list
# ---------------------------------------------------------------------------
MODELS = [
("claude-sonnet-4-6", "推荐:综合能力最强,速度均衡"),
("claude-opus-4-6", "能力最强,适合复杂任务,较慢"),
("claude-haiku-4-5", "速度最快,适合简单任务"),
]
KEEP_CURRENT = "(保持当前设置)"
KEEP_UNCHANGED = "(保持不变)"
def model_display_options(include_keep_current=False, include_keep_unchanged=False):
lines = []
for m, desc in MODELS:
lines.append(f"{m:<25} {dim(desc)}")
if include_keep_current:
lines.append(KEEP_CURRENT)
if include_keep_unchanged:
lines.append(KEEP_UNCHANGED)
return lines
def model_values(include_keep_current=False, include_keep_unchanged=False):
vals = [m for m, _ in MODELS]
if include_keep_current:
vals.append(None) # None = keep current
if include_keep_unchanged:
vals.append(None)
return vals
# ---------------------------------------------------------------------------
# Step 1: Default model selection
# ---------------------------------------------------------------------------
def step1_default_model(config):
print_step(1, 4, "选择默认 Model")
print()
print("┌─────────────────────────────────────────────────────┐")
print("│ 选择 OpenClaw 默认 Model │")
print("│ │")
print("│ claude-sonnet-4-6 ← 推荐:综合能力最强,速度均衡 │")
print("│ claude-opus-4-6 能力最强,适合复杂任务,较慢 │")
print("│ claude-haiku-4-5 速度最快,适合简单任务 │")
print("│ (保持当前设置) │")
print("└─────────────────────────────────────────────────────┘")
print()
current = (config
.get("agents", {})
.get("defaults", {})
.get("model", {})
.get("primary", None))
if current:
print(f" {dim('当前默认 Model:')} {cyan(current)}")
else:
print(f" {dim('当前默认 Model:')} {yellow('(未设置)')}")
print()
# Find default selection index
default_idx = len(MODELS) # default: keep current
if current:
for i, (m, _) in enumerate(MODELS):
if m == current:
default_idx = i
break
options = model_display_options(include_keep_current=True)
print(f" {bold('使用 ↑↓ 方向键选择,Enter 确认:')}")
print()
idx = arrow_select(None, options, default_index=default_idx)
if idx >= len(MODELS):
# Keep current
chosen_model = current # may be None
print(f"\n {green('')} 保持当前设置: {cyan(current) if current else yellow('(未设置)')}")
return chosen_model, False # (value, changed)
else:
chosen_model = MODELS[idx][0]
changed = (chosen_model != current)
print(f"\n {green('')} 选择: {cyan(chosen_model)}")
return chosen_model, changed
# ---------------------------------------------------------------------------
# Step 2: Agent model configuration
# ---------------------------------------------------------------------------
def step2_agent_config(config, default_model):
print_step(2, 4, "Agent Model 配置")
print()
agent_options = [
"不修改任何 Agent(默认)",
"全部按默认 Model 修改",
"逐一配置每个 Agent",
]
print(f" {bold('选择配置方式:')}")
print()
mode_idx = arrow_select(None, agent_options, default_index=0)
print()
agents = config.get("agents", {}).get("list", [])
agent_changes = {} # agent_id -> {"primary": ..., "fallback": ...}
if mode_idx == 0:
print(f" {green('')} 不修改任何 Agent")
return agent_changes
if mode_idx == 1:
# Apply default model to all agents
if not default_model:
print(f" {yellow('⚠️ 未设置默认 Model,跳过批量修改')}")
return agent_changes
for agent in agents:
aid = agent.get("id", "")
if not aid:
continue
current_primary = (agent.get("model", {}) or {}).get("primary", None)
if current_primary != default_model:
agent_changes[aid] = {"primary": default_model, "fallback": None, "keep_fallback": True}
print(f" {green('')} 已将 {len(agent_changes)} 个 Agent 的 primary 设为 {cyan(default_model)}")
return agent_changes
# mode_idx == 2: configure one by one
if not agents:
print(f" {yellow('openclaw.json 中没有找到 Agent 列表')}")
return agent_changes
for agent in agents:
aid = agent.get("id", "")
aname = agent.get("name", aid)
if not aid:
continue
cur_model_block = agent.get("model", {}) or {}
cur_primary = cur_model_block.get("primary", None)
cur_fallback = cur_model_block.get("fallback", None)
print()
print(f" {bold(cyan('Agent:'))} {aname} ({dim(aid)})")
print(f" current primary: {cyan(cur_primary) if cur_primary else yellow('(未设置)')}")
print(f" current fallback: {cyan(cur_fallback) if cur_fallback else yellow('(未设置)')}")
print()
# Pick primary
print(f" {bold('选择新 primary Model (↑↓ Enter):')}")
print()
primary_options = model_display_options(include_keep_unchanged=True)
primary_vals = model_values(include_keep_unchanged=True)
# Default: keep unchanged
p_default = len(MODELS)
if cur_primary:
for i, (m, _) in enumerate(MODELS):
if m == cur_primary:
p_default = i
break
p_idx = arrow_select(None, primary_options, default_index=p_default)
new_primary = primary_vals[p_idx] if p_idx < len(MODELS) else cur_primary
print(f"\n {green('')} primary: {cyan(new_primary) if new_primary else yellow('(保持不变)')}")
# Pick fallback
print()
print(f" {bold('选择新 fallback Model (↑↓ Enter):')}")
print()
fallback_options = model_display_options() + ["(跳过 / 不设置 fallback"]
f_default = len(MODELS) # skip
if cur_fallback:
for i, (m, _) in enumerate(MODELS):
if m == cur_fallback:
f_default = i
break
f_idx = arrow_select(None, fallback_options, default_index=f_default)
if f_idx < len(MODELS):
new_fallback = MODELS[f_idx][0]
else:
new_fallback = cur_fallback # keep existing
print(f"\n {green('')} fallback: {cyan(new_fallback) if new_fallback else yellow('(保持不变)')}")
# Record changes only if something actually differs
primary_changed = (new_primary is not None and new_primary != cur_primary)
fallback_changed = (new_fallback is not None and new_fallback != cur_fallback)
if primary_changed or fallback_changed:
agent_changes[aid] = {
"primary": new_primary if primary_changed else cur_primary,
"fallback": new_fallback if fallback_changed else cur_fallback,
}
# Ask whether to continue to next agent
print()
try:
cont = input(f" 还需要修改其他 Agent 吗?[Y/n]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
if cont in ("n", "no"):
break
return agent_changes
# ---------------------------------------------------------------------------
# Step 3: Summary + OCP check + confirm
# ---------------------------------------------------------------------------
OCP_MODELS_NEEDING_PROXY = ("claude-local", "claude-sonnet", "claude-opus", "claude-haiku")
def _needs_ocp(model_name):
if not model_name:
return False
return any(kw in model_name for kw in OCP_MODELS_NEEDING_PROXY)
def _ocp_running():
try:
result = subprocess.run(
["curl", "-s", "--max-time", "3", "http://localhost:3456/health"],
capture_output=True, text=True
)
return result.returncode == 0 and result.stdout.strip() != ""
except Exception:
return False
def step3_summary_confirm(config, new_default_model, default_changed, agent_changes):
print_step(3, 4, "确认修改")
print()
# Build summary lines
current_default = (config
.get("agents", {})
.get("defaults", {})
.get("model", {})
.get("primary", None))
has_any_change = default_changed or bool(agent_changes)
if not has_any_change:
return False # caller will handle "no changes" message
print(f" {bold('即将应用以下修改:')}")
print()
if default_changed:
old_str = cyan(current_default) if current_default else yellow("(未设置)")
new_str = cyan(new_default_model)
print(f" {bold('[defaults]')} {old_str}{new_str}")
agents = config.get("agents", {}).get("list", [])
agent_map = {a.get("id", ""): a for a in agents if a.get("id")}
for aid, change in agent_changes.items():
agent = agent_map.get(aid, {})
cur_primary = (agent.get("model", {}) or {}).get("primary", None)
new_primary = change.get("primary", cur_primary)
old_str = cyan(cur_primary) if cur_primary else yellow("(未设置)")
new_str = cyan(new_primary) if new_primary else yellow("(未设置)")
aname = agent.get("name", aid)
if cur_primary == new_primary:
print(f" {bold(aname + ':')}{'':4}不变")
else:
print(f" {bold(aname + ':')}{'':4}{old_str}{new_str}")
print()
# OCP check
all_new_models = []
if default_changed and new_default_model:
all_new_models.append(new_default_model)
for aid, change in agent_changes.items():
if change.get("primary"):
all_new_models.append(change["primary"])
if change.get("fallback"):
all_new_models.append(change["fallback"])
needs_ocp = any(_needs_ocp(m) for m in all_new_models)
if needs_ocp and not _ocp_running():
print(f" {yellow('⚠️ 检测到你选择的 Model 需要 openclaw-claude-proxy (ocp)')}")
print(f" {yellow(' 但 ocp 目前未运行。建议先安装并启动 ocp。')}")
print()
try:
ans = input(f" 继续 [c] / 退出查看 ocp 安装文档 [q]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print(f"\n\n{yellow('已取消,未做任何修改')}")
sys.exit(0)
if ans in ("q", "quit", "exit"):
print()
print(f" {cyan('请参考 ocp 安装文档: https://github.com/openclaw/openclaw-claude-proxy')}")
print(f" {yellow('已退出,未做任何修改')}")
sys.exit(0)
confirmed = yes_no(" 确认应用?", default_yes=True)
return confirmed
# ---------------------------------------------------------------------------
# Apply changes to openclaw.json
# ---------------------------------------------------------------------------
def apply_changes(config, new_default_model, default_changed, agent_changes):
if default_changed and new_default_model is not None:
agents_block = config.setdefault("agents", {})
defaults_block = agents_block.setdefault("defaults", {})
model_block = defaults_block.setdefault("model", {})
model_block["primary"] = new_default_model
if agent_changes:
agents_list = config.get("agents", {}).get("list", [])
for agent in agents_list:
aid = agent.get("id", "")
if aid in agent_changes:
change = agent_changes[aid]
if "model" not in agent or agent["model"] is None:
agent["model"] = {}
if change.get("primary") is not None:
agent["model"]["primary"] = change["primary"]
if change.get("fallback") is not None:
agent["model"]["fallback"] = change["fallback"]
# ---------------------------------------------------------------------------
# Step 4: Restart gateway
# ---------------------------------------------------------------------------
def step4_restart_gateway():
print_step(4, 4, "重启 Gateway")
print()
restart = yes_no(" 配置已更新。是否立即重启 Gateway 以使配置生效?", default_yes=True)
if not restart:
print(f" {dim('跳过重启。请手动运行: openclaw gateway restart')}")
return
print(f" {cyan('正在执行: openclaw gateway restart ...')}")
try:
result = subprocess.run(
["openclaw", "gateway", "restart"],
capture_output=True, text=True
)
if result.returncode == 0:
print(f" {green('✓ Gateway 重启成功')}")
if result.stdout.strip():
print(f" {dim(result.stdout.strip())}")
else:
print(f" {yellow('⚠️ Gateway 重启返回非零退出码')}")
if result.stderr.strip():
print(f" {red(result.stderr.strip())}")
except FileNotFoundError:
print(f" {yellow('⚠️ 未找到 openclaw 命令。请手动重启 Gateway。')}")
except Exception as e:
print(f" {red(f'重启失败: {e}')}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
print()
print(bold(cyan("╔══════════════════════════════════════════════════════╗")))
print(bold(cyan("║ OpenClaw Memory-Continuity 配置向导 ║")))
print(bold(cyan("╚══════════════════════════════════════════════════════╝")))
print()
# Check if stdin is a tty — if not, skip interactive wizard
if not sys.stdin.isatty():
print(f" {yellow('非交互式终端,跳过配置向导')}")
sys.exit(0)
# Find config file
config_path = find_openclaw_json()
if not config_path:
print(f" {red('错误: 未找到 openclaw.json')}")
print(f" {dim('已搜索以下路径:')}")
for p in OPENCLAW_JSON_PATHS:
print(f" {dim(p)}")
print(f" {yellow('请先初始化 OpenClaw 配置,再运行此向导')}")
sys.exit(1)
print(f" {dim('配置文件:')} {cyan(config_path)}")
config = load_config(config_path)
# ── Step 1 ──────────────────────────────────────────────────────────────
new_default_model, default_changed = step1_default_model(config)
# ── Step 2 ──────────────────────────────────────────────────────────────
agent_changes = step2_agent_config(config, new_default_model)
# ── Check for any changes ───────────────────────────────────────────────
has_any_change = default_changed or bool(agent_changes)
if not has_any_change:
print()
print(f" {green('无需修改,配置保持不变 ✅')}")
print()
sys.exit(0)
# ── Step 3 ──────────────────────────────────────────────────────────────
confirmed = step3_summary_confirm(
config, new_default_model, default_changed, agent_changes
)
if not confirmed:
print()
print(f" {yellow('已取消,未做任何修改')}")
print()
sys.exit(0)
# Write changes
apply_changes(config, new_default_model, default_changed, agent_changes)
save_config(config_path, config)
print(f"\n {green('✓ 配置已写入')} {cyan(config_path)}")
# ── Step 4 ──────────────────────────────────────────────────────────────
step4_restart_gateway()
print()
print(bold(green("╔══════════════════════════════════════════════════════╗")))
print(bold(green("║ 配置向导完成 ✅ ║")))
print(bold(green("╚══════════════════════════════════════════════════════╝")))
print()
if __name__ == "__main__":
main()
+381
View File
@@ -0,0 +1,381 @@
#!/usr/bin/env bash
# verify.sh — Verify memory-continuity installation (3-layer check)
#
# Usage:
# bash scripts/verify.sh [--workspace PATH] [--sample] [--all-agents]
#
# Options:
# --workspace PATH Override default workspace (~/.openclaw/workspace/main)
# --sample Show a sample high-importance CURRENT_STATE.md
# --all-agents Run 3-layer check across all detected agent workspaces
set -euo pipefail
# ---------------------------------------------------------------------------
# Colors
# ---------------------------------------------------------------------------
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
ok() { echo -e " ${GREEN}$*${RESET}"; }
warn() { echo -e " ${YELLOW}⚠️ $*${RESET}"; }
fail() { echo -e " ${RED}$*${RESET}"; }
info() { echo -e " ${CYAN}$*${RESET}"; }
header() { echo -e "\n${BOLD}$*${RESET}"; }
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
WORKSPACE="${HOME}/.openclaw/workspace/main"
WORKSPACE_EXPLICIT=false
SHOW_SAMPLE=false
ALL_AGENTS=false
while [[ $# -gt 0 ]]; do
case "$1" in
--workspace)
WORKSPACE="$2"; WORKSPACE_EXPLICIT=true; shift 2 ;;
--sample)
SHOW_SAMPLE=true; shift ;;
--all-agents)
ALL_AGENTS=true; shift ;;
*)
echo "Unknown argument: $1" >&2; exit 1 ;;
esac
done
# Expand ~ in workspace path
WORKSPACE="${WORKSPACE/#\~/$HOME}"
# ---------------------------------------------------------------------------
# --sample: print example state and exit
# ---------------------------------------------------------------------------
if $SHOW_SAMPLE; then
echo -e "\n${BOLD}Sample CURRENT_STATE.md (high-importance production task):${RESET}\n"
cat <<'EOF'
# Current State
> Last updated: 2026-03-19T11:42:00Z
## Objective
Complete production database migration v2→v3 for user activity table (sharded, ~80M rows).
## Current Step
Migration script written and reviewed. Dry-run on staging passed.
**Not yet tested against production replica.**
## Key Decisions
- Using online schema change (pt-online-schema-change) to avoid table lock
- Backfill batch size: 5,000 rows / 500ms to stay under replication lag threshold
- Rollback plan: swap back via feature flag, no destructive drop until T+48h
## Next Action
Schedule test run against prod replica — waiting for Tao to confirm migration
window (UTC+10, Thursday 23:00). Do NOT proceed without explicit sign-off.
## Blockers
- ⚠️ BLOCKED: Tao hasn't confirmed migration window (UTC+10 Thu 23:00)
- ⚠️ Prod replica test not yet scheduled
## Risk
- Skipping production replica test → potential data loss on edge cases not covered by staging schema
- Migration window must be off-peak; violating this risks exceeding replication lag SLA (>30s triggers alert)
## Unsurfaced Results
- Staging dry-run log: /tmp/pt-osc-staging-2026-03-19.log (not yet reviewed for warnings)
EOF
exit 0
fi
# ---------------------------------------------------------------------------
# --all-agents: detect all agent workspaces and run checks for each
# ---------------------------------------------------------------------------
OPENCLAW_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
CONFIG_FILE="$OPENCLAW_DIR/openclaw.json"
PLUGIN_DIR_SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
detect_all_agent_workspaces() {
if [[ ! -f "$CONFIG_FILE" ]] || ! command -v python3 &>/dev/null; then
return 1
fi
python3 - "$CONFIG_FILE" "$OPENCLAW_DIR" <<'PYEOF'
import json, sys, os
config_file = sys.argv[1]
openclaw_dir = sys.argv[2]
with open(config_file) as f:
data = json.load(f)
default_ws = data.get('agents', {}).get('defaults', {}).get('workspace', os.path.join(openclaw_dir, 'workspace', 'main'))
seen = set()
for agent in data.get('agents', {}).get('list', []):
agent_id = agent.get('id', '')
if not agent_id or agent_id in seen:
continue
seen.add(agent_id)
name = agent.get('name', agent_id)
workspace = agent.get('workspace', default_ws if agent_id == 'main' else os.path.join(openclaw_dir, 'workspaces', agent_id))
workspace = os.path.expanduser(workspace)
print('{}|{}|{}'.format(agent_id, name, workspace))
PYEOF
}
if $ALL_AGENTS; then
GRAY='\033[0;90m'
echo -e "\n${BOLD}memory-continuity — Multi-Agent Verifier${RESET}"
echo "────────────────────────────────────────────"
AGENT_LINES=()
while IFS= read -r line; do
AGENT_LINES+=("$line")
done < <(detect_all_agent_workspaces 2>/dev/null || true)
if [[ ${#AGENT_LINES[@]} -eq 0 ]]; then
echo -e "${RED}No agents detected. Is openclaw.json present and python3 available?${RESET}"
exit 1
fi
# Filter to alive agents (workspace directory exists)
ALIVE_LINES=()
SKIPPED=0
for line in "${AGENT_LINES[@]}"; do
IFS='|' read -r agent_id agent_name agent_ws <<< "$line"
if [[ -d "$agent_ws" ]]; then
ALIVE_LINES+=("$line")
else
echo -e " ${GRAY}Skipping ${agent_name} (${agent_id}) — workspace not found: ${agent_ws}${RESET}"
SKIPPED=$((SKIPPED + 1))
fi
done
if [[ $SKIPPED -gt 0 ]]; then
echo -e " ${GRAY}Skipped ${SKIPPED} agent(s) with missing workspace directories${RESET}"
fi
if [[ ${#ALIVE_LINES[@]} -eq 0 ]]; then
echo -e "${YELLOW}All ${#AGENT_LINES[@]} agent(s) in config have missing workspace directories.${RESET}"
echo -e "${YELLOW}You may need to initialize them first.${RESET}"
exit 1
fi
TOTAL_PASS=0
TOTAL_FAIL=0
for line in "${ALIVE_LINES[@]}"; do
IFS='|' read -r agent_id agent_name agent_ws <<< "$line"
echo -e "\n${BOLD}Agent: ${agent_name} (${agent_id})${RESET}"
echo -e " Workspace: ${agent_ws}"
AGENT_ERRORS=0
AGENT_WARNINGS=0
# Layer 1: SKILL.md in workspace
SKILL_PATH="${agent_ws}/skills/memory-continuity/SKILL.md"
if [[ -f "$SKILL_PATH" ]]; then
ok "SKILL.md installed"
else
fail "SKILL.md missing at ${SKILL_PATH}"
AGENT_ERRORS=$((AGENT_ERRORS + 1))
fi
# Layer 2: continuity_doctor.py
DOCTOR="${PLUGIN_DIR_SELF}/scripts/continuity_doctor.py"
if command -v python3 &>/dev/null && [[ -f "$DOCTOR" ]]; then
if python3 "$DOCTOR" --workspace "$agent_ws" &>/dev/null; then
ok "continuity_doctor.py passed"
else
warn "continuity_doctor.py exited non-zero"
AGENT_WARNINGS=$((AGENT_WARNINGS + 1))
fi
else
warn "python3 or continuity_doctor.py unavailable — skipped"
AGENT_WARNINGS=$((AGENT_WARNINGS + 1))
fi
# Layer 3: CURRENT_STATE.md
STATE_FILE="${agent_ws}/memory/CURRENT_STATE.md"
if [[ ! -f "$STATE_FILE" ]]; then
warn "CURRENT_STATE.md not found (normal for new installs)"
AGENT_WARNINGS=$((AGENT_WARNINGS + 1))
else
LINE_COUNT=$(grep -c '[^[:space:]]' "$STATE_FILE" || true)
if [[ $LINE_COUNT -gt 2 ]]; then
ok "CURRENT_STATE.md present (${LINE_COUNT} non-blank lines)"
else
warn "CURRENT_STATE.md appears empty or placeholder"
AGENT_WARNINGS=$((AGENT_WARNINGS + 1))
fi
fi
if [[ $AGENT_ERRORS -eq 0 ]]; then
echo -e " ${GREEN}→ PASS${RESET} (${AGENT_WARNINGS} warning(s))"
TOTAL_PASS=$((TOTAL_PASS + 1))
else
echo -e " ${RED}→ FAIL${RESET} (${AGENT_ERRORS} error(s), ${AGENT_WARNINGS} warning(s))"
TOTAL_FAIL=$((TOTAL_FAIL + 1))
fi
done
echo ""
echo "────────────────────────────────────────────"
SUMMARY_DETAIL=""
[[ $SKIPPED -gt 0 ]] && SUMMARY_DETAIL=", ${SKIPPED} skipped (no workspace)"
echo -e "${BOLD}Summary: ${TOTAL_PASS} passed, ${TOTAL_FAIL} failed (of ${#ALIVE_LINES[@]} alive agents${SUMMARY_DETAIL})${RESET}"
[[ $TOTAL_FAIL -gt 0 ]] && exit 1 || exit 0
fi
# ---------------------------------------------------------------------------
# Resolve WORKSPACE from openclaw.json if not explicitly set via --workspace
# ---------------------------------------------------------------------------
# If not explicitly set, try to derive from the first alive agent in openclaw.json
if ! $WORKSPACE_EXPLICIT && [[ -f "$CONFIG_FILE" ]] && command -v python3 &>/dev/null; then
RESOLVED_WS=$(python3 - "$CONFIG_FILE" "$OPENCLAW_DIR" <<'PYEOF'
import json, sys, os
config_file = sys.argv[1]
openclaw_dir = sys.argv[2]
try:
with open(config_file) as f:
data = json.load(f)
default_ws = data.get('agents', {}).get('defaults', {}).get('workspace', '')
for agent in data.get('agents', {}).get('list', []):
ws = agent.get('workspace', default_ws)
if ws:
print(os.path.expanduser(ws))
break
except Exception:
pass
PYEOF
)
if [[ -n "$RESOLVED_WS" ]]; then
WORKSPACE="$RESOLVED_WS"
fi
fi
# ---------------------------------------------------------------------------
# Header
# ---------------------------------------------------------------------------
echo -e "\n${BOLD}memory-continuity — Installation Verifier${RESET}"
echo "────────────────────────────────────────────"
ERRORS=0
WARNINGS=0
# ===========================================================================
# Layer 1: Required files exist
# ===========================================================================
header "Layer 1: Required files"
PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
check_file() {
local rel="$1"
local path="${PLUGIN_DIR}/${rel}"
if [[ -f "$path" ]]; then
ok "$rel"
else
fail "$rel — NOT FOUND (expected at ${path})"
ERRORS=$((ERRORS + 1))
fi
}
check_file "SKILL.md"
check_file "scripts/continuity_doctor.py"
check_file "openclaw.plugin.json"
# ===========================================================================
# Layer 2: continuity_doctor.py runs without error
# ===========================================================================
header "Layer 2: Tool availability"
if ! command -v python3 &>/dev/null; then
fail "python3 not found in PATH"
ERRORS=$((ERRORS + 1))
else
ok "python3 found ($(python3 --version 2>&1))"
DOCTOR="${PLUGIN_DIR}/scripts/continuity_doctor.py"
if [[ ! -f "$DOCTOR" ]]; then
fail "continuity_doctor.py missing — skipping doctor check"
ERRORS=$((ERRORS + 1))
else
info "Running: python3 scripts/continuity_doctor.py --workspace ${WORKSPACE}"
echo ""
if python3 "$DOCTOR" --workspace "$WORKSPACE" 2>&1 | sed 's/^/ /'; then
echo ""
ok "continuity_doctor.py exited cleanly"
else
echo ""
warn "continuity_doctor.py exited with non-zero status (see output above)"
WARNINGS=$((WARNINGS + 1))
fi
fi
fi
# ===========================================================================
# Layer 3: CURRENT_STATE.md content check
# ===========================================================================
header "Layer 3: Workspace state"
STATE_FILE="${WORKSPACE}/memory/CURRENT_STATE.md"
if [[ ! -f "$STATE_FILE" ]]; then
warn "CURRENT_STATE.md not found at: ${STATE_FILE}"
warn "This is normal for a brand-new install — state will be created after your first session."
WARNINGS=$((WARNINGS + 1))
else
# Detect placeholder / trivial content:
# - File is empty
# - Only contains the header line "# Current State"
# - Contains the phrase "No active" or "placeholder"
CONTENT=$(cat "$STATE_FILE")
LINE_COUNT=$(echo "$CONTENT" | grep -c '[^[:space:]]' || true)
IS_PLACEHOLDER=false
if [[ $LINE_COUNT -le 2 ]]; then
IS_PLACEHOLDER=true
elif ! echo "$CONTENT" | grep -qE '##\s+(Objective|Current Step|In Flight|Next Action|Blocked)'; then
# Has some lines but none of the expected structured sections
IS_PLACEHOLDER=true
else
# Check if the Objective section itself is trivial (placeholder text right after the heading)
OBJECTIVE_VALUE=$(echo "$CONTENT" | awk '/^## Objective/{found=1; next} found && /^##/{exit} found && /[^[:space:]]/{print; exit}')
if echo "$OBJECTIVE_VALUE" | grep -qiE '^\s*(no active|placeholder|todo|tbd|empty|n\/a|none|untitled)$'; then
IS_PLACEHOLDER=true
fi
fi
if $IS_PLACEHOLDER; then
echo ""
warn "CURRENT_STATE.md exists but appears to be empty or a placeholder."
warn "The plugin is installed correctly, but your working state hasn't been"
warn "captured yet. After your first real work session it will be populated"
warn "automatically."
echo ""
warn "To see what a high-importance state entry looks like, run:"
echo -e " ${CYAN}bash scripts/verify.sh --sample${RESET}"
WARNINGS=$((WARNINGS + 1))
else
ok "CURRENT_STATE.md found with substantive content (${LINE_COUNT} non-blank lines)"
# Show last-updated line if present
UPDATED=$(grep -m1 'Last updated' "$STATE_FILE" || true)
[[ -n "$UPDATED" ]] && info "$UPDATED"
fi
fi
# ===========================================================================
# Summary
# ===========================================================================
echo ""
echo "────────────────────────────────────────────"
if [[ $ERRORS -eq 0 && $WARNINGS -eq 0 ]]; then
echo -e "${GREEN}${BOLD}All checks passed. memory-continuity is correctly installed.${RESET}"
elif [[ $ERRORS -eq 0 ]]; then
echo -e "${YELLOW}${BOLD}Checks passed with ${WARNINGS} warning(s). Review warnings above.${RESET}"
else
echo -e "${RED}${BOLD}${ERRORS} error(s), ${WARNINGS} warning(s). Installation may be incomplete.${RESET}"
echo -e "${RED}Re-run the installer: bash scripts/post-install.sh${RESET}"
fi
echo ""