commit 46db8ab0b9518c5469ea2dfba748da68c4e371dd Author: dtzp555-max Date: Wed Mar 11 22:21:31 2026 +0000 v0.2: rewrite skill with discipline protocol, doctor, and archive diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..925d4f0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 dtzp555-max + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..701f622 --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +# memory-continuity + +OpenClaw skill for **short-term working continuity** — so your agent can pick up +exactly where it left off after a gateway crash, `/new`, model fallback, or +context compaction. + +## What problem does this solve? + +When your gateway dies mid-conversation, or you hit `/new` to start fresh, or +the model falls back to a different provider — your agent loses its working +context. Long-term memory can tell it *what it knows*, but not *what it was +doing*. This skill fills that gap. + +**One-line summary:** Long-term memory = what you know. This skill = what you +are doing right now. + +## Quick Start + +### Install + +```bash +# Go to your OpenClaw workspace skills directory +cd ~/.openclaw/workspace/skills/ + +# Clone +git clone https://github.com/dtzp555-max/memory-continuity.git + +# That's it. No npm install, no API keys, no database. +``` + +### Test it + +1. Start a conversation with your agent about a multi-step task +2. Chat for a few turns, make some decisions +3. Check: does `memory/CURRENT_STATE.md` exist in your workspace? Does it + reflect what you were doing? +4. Type `/new` to start a fresh session +5. The agent should read `CURRENT_STATE.md` and ask: + *"Last session we were working on X. Want to continue?"* + +If step 5 works, the skill is doing its job. + +### Run the doctor + +```bash +python3 scripts/continuity_doctor.py --workspace ~/.openclaw/workspace +``` + +Sample output: +``` +Continuity Doctor — scanning: /Users/you/.openclaw/workspace +============================================================ + +[OK] memory/CURRENT_STATE.md exists +[OK] CURRENT_STATE.md is fresh (0.3h old) +[OK] Template compliance: all sections present +[WARNING] Unsurfaced Results section is not empty — review needed +[INFO] Found 3 session archive(s), latest: 2026-03-12_14-30.md + +Overall status: WARNING +``` + +## How it works + +The skill installs a behavioral protocol via `SKILL.md`. When loaded, the agent +follows these rules: + +1. **Session start:** Read `memory/CURRENT_STATE.md`, brief the user, wait for + confirmation +2. **During work:** Overwrite the state file at key moments (decisions, + completed steps, errors, before long tool calls, every ~10 turns) +3. **Session end / `/new`:** Final state save + archive a timestamped snapshot + +The state file uses a fixed template: + +```markdown +# Current State +> Last updated: 2026-03-12T14:30:00Z + +## Objective +Build the user authentication module + +## Current Step +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 + +## Blockers +None + +## Unsurfaced Results +None +``` + +The entire file is designed to be read in 15 seconds. It is overwritten (not +appended) on every update, keeping it permanently short. + +## Architecture position + +This skill occupies a specific niche. Here is how it relates to other tools: + +| Layer | Tool | What it stores | +|---|---|---| +| Working state | **memory-continuity** (this skill) | What you are doing *right now* | +| Stable facts | OpenClaw native markdown memory | Preferences, decisions, knowledge | +| Retrieval | memory-lancedb-pro / similar | Searchable long-term history | + +These layers are complementary. This skill has **zero dependency** on any +database or external memory plugin. It works with plain markdown files that +live in your workspace and can be backed up with `git` or `cp`. + +## File structure + +``` +memory-continuity/ +├── SKILL.md # Core skill (loaded by OpenClaw) +├── README.md # This file +├── LICENSE # MIT +├── references/ +│ ├── template.md # CURRENT_STATE.md blank template +│ └── doctor-spec.md # Doctor check specifications +└── scripts/ + └── continuity_doctor.py # Diagnostic tool (reports only, no auto-repair) +``` + +At runtime, the skill creates these files in your workspace: + +``` +$WORKSPACE/ +└── memory/ + ├── CURRENT_STATE.md # Live workbench (overwritten each update) + └── session_archive/ # Timestamped snapshots from past sessions + ├── 2026-03-12_14-30.md + └── ... +``` + +## Design principles + +1. **Zero dependencies.** No database, no API, no npm packages. Just files. +2. **Backup = copy.** The entire state is in `memory/`. Back it up however you + back up your workspace. +3. **Overwrite, don't append.** CURRENT_STATE.md is a workbench, not a journal. + It stays short because it is replaced on every update. +4. **Diagnose, don't auto-repair.** The doctor script flags problems for you to + fix. Automated repair of state files is too risky at this stage. +5. **Complement, don't compete.** This skill does not replace long-term memory. + It solves a different problem (crash recovery vs knowledge retrieval). + +## Current status + +**v0.2 — Draft / early version** + +What works: +- SKILL.md protocol with discipline rules +- CURRENT_STATE.md template +- Continuity doctor diagnostic script +- Session archive on `/new` + +Planned: +- Plugin version with `command:new` and `before_agent_start` hooks + (for guaranteed save/restore without relying on agent self-discipline) +- More real-world validation across different gateway configurations +- Sub-agent continuity handover protocol + +## License + +MIT diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..25f0fab --- /dev/null +++ b/SKILL.md @@ -0,0 +1,159 @@ +--- +name: memory-continuity +description: > + Short-term working continuity for OpenClaw agents. Preserves in-flight state + across gateway restarts, /new, model fallback, and context compaction. + Use this skill whenever an agent needs to survive session breaks without + losing what it was doing. Triggers on: session start, /new, context recovery, + "where were we", "continue", resuming work, or any situation where recent + working state may have been lost. This is NOT a long-term memory system — + it is a crash-recovery workbench. +--- + +# memory-continuity + +Lightweight continuity layer that keeps a single overwrite-oriented state file +(`memory/CURRENT_STATE.md`) so any agent can pick up exactly where it left off +after a restart, `/new`, gateway crash, model fallback, or context compaction. + +## Why this exists + +Long-term memory (vector DB, markdown journals) stores *what you know*. +This skill stores *what you are doing right now*. They solve different problems. + +When a gateway crashes mid-task, no amount of long-term memory recall can tell +the next session: "you were halfway through step 3, the user approved option B, +and the blocker was X." That is what CURRENT_STATE.md does. + +## File layout + +``` +$WORKSPACE/ +├── memory/ +│ ├── CURRENT_STATE.md # THE workbench (overwritten, never appended) +│ └── session_archive/ # compressed snapshots from past sessions +│ ├── 2026-03-12_14-30.md +│ └── ... +``` + +--- + +## MANDATORY PROTOCOL + +### 1. On every session start + +``` +IF memory/CURRENT_STATE.md exists: + READ it + Tell user: "Last session we were working on [Objective]. We reached [Current Step]. Want to continue?" + WAIT for user confirmation before proceeding +ELSE: + Create memory/CURRENT_STATE.md with empty template (see below) +``` + +### 2. When to update CURRENT_STATE.md (the discipline rules) + +Update the file by **overwriting** it (not appending) at these moments: + +| Trigger | Why | +|---|---| +| User confirms a decision | Decisions are the hardest thing to reconstruct | +| A task step completes | Marks progress so next session knows where to start | +| An error or blocker appears | Prevents the next session from hitting the same wall | +| Before any tool call that might take long | If gateway dies during the call, state is already saved | +| User says "let's stop here" or similar | Explicit save point | +| Every ~10 turns of substantive conversation | Periodic checkpoint against silent context loss | + +**The update must be quick.** Write only what changed. The entire file should +stay under 40 lines. If you find yourself writing more, you are journaling, +not checkpointing. Stop and compress. + +### 3. On `/new` or session end + +Before the session closes: + +1. Do a final overwrite of `memory/CURRENT_STATE.md` with latest state +2. Copy a timestamped snapshot to `memory/session_archive/YYYY-MM-DD_HH-MM.md` +3. The snapshot is a frozen record; CURRENT_STATE.md is the live workbench + +If gateway crashes before you can do this, that is OK — the last mid-session +checkpoint in CURRENT_STATE.md is your recovery point. It will not be perfect, +but it will be vastly better than starting from zero. + +### 4. Result surfacing rule + +If you are a sub-agent or execution agent: +- Before exiting, write your key results into CURRENT_STATE.md under `## Unsurfaced Results` +- The main agent MUST check this section on startup and relay findings to the user + +This prevents the #1 silent failure: sub-agent did the work, but nobody saw it. + +--- + +## CURRENT_STATE.md Template + +```markdown +# Current State +> Last updated: [ISO timestamp] + +## Objective +[One sentence: what are we trying to accomplish] + +## Current Step +[What step are we on, what was the last thing completed] + +## Key Decisions +- [Decision 1: what was decided and why, max 3 items] + +## Next Action +[Exactly what should happen next] + +## Blockers +[What is preventing progress, or "None"] + +## Unsurfaced Results +[Results from sub-agents or tools not yet shown to user, or "None"] +``` + +**Rules for this template:** +- Every field is mandatory. Write "None" rather than omitting a section. +- `Objective` and `Next Action` are the two most critical fields. If you can + only save two things before a crash, save these. +- `Key Decisions` caps at 3 items. Older decisions belong in long-term memory, + not here. +- The entire file should be readable in 15 seconds. If it takes longer, trim it. + +--- + +## Continuity Doctor (optional) + +Run `scripts/continuity_doctor.py` to check workspace health: + +```bash +python3 scripts/continuity_doctor.py --workspace /path/to/workspace +``` + +It checks: +- Does `memory/CURRENT_STATE.md` exist? +- Is it stale (not updated in the last session)? +- Does `Objective` match any active tasks file? +- Are there `Unsurfaced Results` that were never addressed? +- Are there session archives without a corresponding state update? + +The doctor **reports only** — it does not auto-repair. You decide what to fix. + +--- + +## What this skill is NOT + +- Not a long-term memory system (use OpenClaw's native markdown memory or LanceDB for that) +- Not a conversation log or journal (CURRENT_STATE.md is overwritten, not appended) +- Not a task manager (use OpenSpec or tasks.md for project planning) +- Not dependent on any external database (works with plain files only) + +## Compatibility + +- Works with any OpenClaw agent (main or sub-agent) +- No external dependencies (no npm install, no API keys, no database) +- Backup: `git add memory/` or `cp -r memory/ /backup/` — that is the entire disaster recovery plan +- Can coexist with memory-lancedb-pro, hippocampus, or any other memory skill diff --git a/memory/.gitkeep b/memory/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/references/doctor-spec.md b/references/doctor-spec.md new file mode 100644 index 0000000..ace8faf --- /dev/null +++ b/references/doctor-spec.md @@ -0,0 +1,64 @@ +# Continuity Doctor — Specification + +## Purpose + +The continuity doctor is a diagnostic tool that checks whether the +memory-continuity protocol is being followed correctly in a workspace. +It reports problems but does **not** auto-fix them. + +## Design philosophy + +- **Diagnose, don't repair.** Automated repair of state files is dangerous + because incorrect "fixes" can overwrite valid state. The doctor flags + issues for human or agent review. +- **Fast and offline.** No API calls, no database queries. Reads files only. +- **Exit codes matter.** 0 = healthy, 1 = warnings found, 2 = critical issues. + +## Checks performed + +### 1. Existence check +- Does `memory/CURRENT_STATE.md` exist? +- Severity: CRITICAL if missing (no recovery possible) + +### 2. Staleness check +- When was `CURRENT_STATE.md` last modified? +- If older than the most recent session transcript, it is stale. +- Severity: WARNING + +### 3. Template compliance +- Does the file contain all mandatory sections? + (Objective, Current Step, Key Decisions, Next Action, Blockers, Unsurfaced Results) +- Are any sections still showing placeholder text like `[One sentence: ...]`? +- Severity: WARNING for missing sections, INFO for placeholder text + +### 4. Unsurfaced results +- Is the `Unsurfaced Results` section non-empty? +- If yes, someone needs to review those results. +- Severity: WARNING + +### 5. Archive consistency +- Are there files in `memory/session_archive/` ? +- Does the newest archive have a different Objective than CURRENT_STATE.md? + (This is expected if the user switched tasks, but worth flagging.) +- Severity: INFO + +### 6. Conflict detection (optional, if tasks file exists) +- If a `tasks.md` or `openspec/` directory exists, does the Objective in + CURRENT_STATE.md align with any active task? +- Severity: INFO (alignment is nice-to-have, not mandatory) + +## Output format + +``` +[CRITICAL] memory/CURRENT_STATE.md does not exist +[WARNING] CURRENT_STATE.md is stale (last modified 2h ago, session ran 30m ago) +[WARNING] Unsurfaced Results section is not empty — review needed +[INFO] Archive objective differs from current objective (task switch?) +[OK] Template compliance: all sections present +``` + +## Future extensions (not yet implemented) + +- Multi-workspace scan (check all sub-agent workspaces) +- JSON output mode for programmatic consumption +- Integration with OpenClaw cron for scheduled health checks diff --git a/references/template.md b/references/template.md new file mode 100644 index 0000000..c5a7584 --- /dev/null +++ b/references/template.md @@ -0,0 +1,20 @@ +# Current State +> Last updated: [ISO timestamp] + +## Objective +[One sentence: what are we trying to accomplish] + +## Current Step +[What step are we on, what was the last thing completed] + +## Key Decisions +- None yet + +## Next Action +[Exactly what should happen next] + +## Blockers +None + +## Unsurfaced Results +None diff --git a/scripts/continuity_doctor.py b/scripts/continuity_doctor.py new file mode 100644 index 0000000..9f0ee91 --- /dev/null +++ b/scripts/continuity_doctor.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +""" +continuity_doctor.py — Diagnostic tool for memory-continuity skill. + +Checks workspace health and reports issues. Does NOT auto-repair. + +Usage: + python3 continuity_doctor.py --workspace /path/to/workspace + python3 continuity_doctor.py --workspace ~/.openclaw/workspace/main +""" + +import argparse +import os +import sys +import re +from datetime import datetime, timezone +from pathlib import Path + + +# --------------------------------------------------------------------------- +# Severity levels +# --------------------------------------------------------------------------- +class Severity: + OK = "OK" + INFO = "INFO" + WARNING = "WARNING" + CRITICAL = "CRITICAL" + + +# --------------------------------------------------------------------------- +# Collector +# --------------------------------------------------------------------------- +class DiagnosticReport: + def __init__(self): + self.entries: list[tuple[str, str]] = [] + self._worst = Severity.OK + + def add(self, severity: str, message: str): + self.entries.append((severity, message)) + rank = {Severity.OK: 0, Severity.INFO: 1, Severity.WARNING: 2, Severity.CRITICAL: 3} + if rank.get(severity, 0) > rank.get(self._worst, 0): + self._worst = severity + + def print_report(self): + for severity, message in self.entries: + tag = f"[{severity}]".ljust(12) + print(f"{tag}{message}") + print() + print(f"Overall status: {self._worst}") + + @property + def exit_code(self) -> int: + if self._worst == Severity.CRITICAL: + return 2 + if self._worst == Severity.WARNING: + return 1 + return 0 + + +# --------------------------------------------------------------------------- +# Required sections in CURRENT_STATE.md +# --------------------------------------------------------------------------- +REQUIRED_SECTIONS = [ + "Objective", + "Current Step", + "Key Decisions", + "Next Action", + "Blockers", + "Unsurfaced Results", +] + +PLACEHOLDER_PATTERNS = [ + r"\[.*?\]", # anything in square brackets like [One sentence: ...] +] + + +# --------------------------------------------------------------------------- +# Checks +# --------------------------------------------------------------------------- + +def check_existence(workspace: Path, report: DiagnosticReport) -> Path | None: + """Check that memory/CURRENT_STATE.md exists.""" + state_file = workspace / "memory" / "CURRENT_STATE.md" + if not state_file.exists(): + report.add(Severity.CRITICAL, "memory/CURRENT_STATE.md does not exist") + return None + report.add(Severity.OK, "memory/CURRENT_STATE.md exists") + return state_file + + +def check_staleness(state_file: Path, workspace: Path, report: DiagnosticReport): + """Check if the state file is older than the most recent activity.""" + mtime = datetime.fromtimestamp(state_file.stat().st_mtime, tz=timezone.utc) + age_seconds = (datetime.now(timezone.utc) - mtime).total_seconds() + age_hours = age_seconds / 3600 + + if age_hours > 24: + report.add( + Severity.WARNING, + f"CURRENT_STATE.md is stale (last modified {age_hours:.1f}h ago)", + ) + elif age_hours > 4: + report.add( + Severity.INFO, + f"CURRENT_STATE.md last modified {age_hours:.1f}h ago", + ) + else: + report.add(Severity.OK, f"CURRENT_STATE.md is fresh ({age_hours:.1f}h old)") + + +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] = {} + + for section in REQUIRED_SECTIONS: + # Match ## Section or ## Section\n + pattern = rf"##\s+{re.escape(section)}\s*\n(.*?)(?=\n##\s|\Z)" + match = re.search(pattern, content, re.DOTALL) + if not match: + report.add(Severity.WARNING, f"Missing section: ## {section}") + continue + + body = match.group(1).strip() + sections_found[section] = body + + # Check for placeholder text + if body and all(re.fullmatch(p, body) for p in PLACEHOLDER_PATTERNS): + report.add(Severity.INFO, f"Section '{section}' still contains placeholder text") + + if len(sections_found) == len(REQUIRED_SECTIONS): + report.add(Severity.OK, "Template compliance: all sections present") + + return sections_found + + +def check_unsurfaced_results(sections: dict, report: DiagnosticReport): + """Check if there are unsurfaced results that need attention.""" + results = sections.get("Unsurfaced Results", "").strip().lower() + if results and results != "none": + report.add( + Severity.WARNING, + "Unsurfaced Results section is not empty — review needed", + ) + else: + report.add(Severity.OK, "No unsurfaced results pending") + + +def check_archive(workspace: Path, sections: dict, report: DiagnosticReport): + """Check session archive consistency.""" + archive_dir = workspace / "memory" / "session_archive" + + if not archive_dir.exists() or not list(archive_dir.glob("*.md")): + report.add(Severity.INFO, "No session archives found (first session?)") + return + + archives = sorted(archive_dir.glob("*.md")) + latest_archive = archives[-1] + report.add(Severity.OK, f"Found {len(archives)} session archive(s), latest: {latest_archive.name}") + + # Compare objectives + current_objective = sections.get("Objective", "").strip() + archive_content = latest_archive.read_text(encoding="utf-8") + obj_match = re.search(r"##\s+Objective\s*\n(.*?)(?=\n##\s|\Z)", archive_content, re.DOTALL) + if obj_match: + archive_objective = obj_match.group(1).strip() + if archive_objective != current_objective and current_objective: + report.add( + Severity.INFO, + "Archive objective differs from current objective (task switch?)", + ) + + +def check_tasks_alignment(workspace: Path, sections: dict, report: DiagnosticReport): + """Optional: check if objective aligns with tasks.md if it exists.""" + tasks_file = workspace / "tasks.md" + if not tasks_file.exists(): + return + + objective = sections.get("Objective", "").strip().lower() + if not objective: + return + + tasks_content = tasks_file.read_text(encoding="utf-8").lower() + # Very rough heuristic: check if any significant word from objective appears in tasks + words = [w for w in objective.split() if len(w) > 4] + matches = sum(1 for w in words if w in tasks_content) + if words and matches == 0: + report.add( + Severity.INFO, + "Objective does not seem to match any content in tasks.md", + ) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def run_doctor(workspace_path: str) -> int: + workspace = Path(workspace_path).expanduser().resolve() + report = DiagnosticReport() + + print(f"Continuity Doctor — scanning: {workspace}") + print("=" * 60) + + if not workspace.exists(): + report.add(Severity.CRITICAL, f"Workspace does not exist: {workspace}") + report.print_report() + return report.exit_code + + # Run all checks + state_file = check_existence(workspace, report) + + if state_file: + check_staleness(state_file, workspace, report) + sections = check_template_compliance(state_file, report) + check_unsurfaced_results(sections, report) + check_archive(workspace, sections, report) + check_tasks_alignment(workspace, sections, report) + + print() + report.print_report() + return report.exit_code + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Continuity Doctor — diagnose memory-continuity health" + ) + parser.add_argument( + "--workspace", + required=True, + help="Path to the OpenClaw workspace to check", + ) + args = parser.parse_args() + sys.exit(run_doctor(args.workspace))