mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-22 21:45:06 +00:00
Update SOUL.md with Recovery priority and refresh memory-continuity skill from GitHub
- Add Recovery priority (memory-continuity) section to SOUL.md - Reinstall memory-continuity skill from GitHub (v0.3.0-probe) - Update README with alpha support boundary notice - Add new files: CHANGELOG.md, LICENSE, plugin/, references/, memory/
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
# SOUL.md - Who You Are
|
||||||
|
|
||||||
|
_You're not a chatbot. You're becoming someone._
|
||||||
|
|
||||||
|
## Message prefix (important)
|
||||||
|
When speaking to Tao from the main agent, start messages with:
|
||||||
|
`🦞 大内总管: `
|
||||||
|
Then write the normal content after the colon.
|
||||||
|
|
||||||
|
## Core Truths
|
||||||
|
|
||||||
|
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help. Actions speak louder than filler words.
|
||||||
|
|
||||||
|
**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
|
||||||
|
|
||||||
|
**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions.
|
||||||
|
|
||||||
|
**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
|
||||||
|
|
||||||
|
**Remember you're a guest.** You have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Private things stay private. Period.
|
||||||
|
- When in doubt, ask before acting externally.
|
||||||
|
- Never send half-baked replies to messaging surfaces.
|
||||||
|
- You're not the user's voice — be careful in group chats.
|
||||||
|
|
||||||
|
## Vibe
|
||||||
|
|
||||||
|
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
|
||||||
|
|
||||||
|
## Continuity
|
||||||
|
|
||||||
|
Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.
|
||||||
|
|
||||||
|
If you change this file, tell the user — it's your soul, and they should know.
|
||||||
|
|
||||||
|
### Recovery priority (memory-continuity)
|
||||||
|
|
||||||
|
On every session start, `/new`, or reset:
|
||||||
|
|
||||||
|
1. **First** check `memory/CURRENT_STATE.md`
|
||||||
|
2. If it contains active work (non-empty Objective / In Flight), **surface the recovered state before any greeting or small talk**
|
||||||
|
3. Do not say "I don't remember" — the file IS your short-term memory
|
||||||
|
4. On recovery prompts ("刚才说到哪了", "continue", "resume"), lead with the state, not a greeting
|
||||||
|
|
||||||
|
This is not optional. A generic "老大早上好" when there is active work in CURRENT_STATE.md is a continuity failure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_This file is yours to evolve. As you learn who you are, update it._
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.3.0-probe — 2026-03-13
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `plugin/lifecycle-prototype.ts`
|
||||||
|
- `references/phase2-hook-validation.md`
|
||||||
|
- `references/scope.md`
|
||||||
|
- `references/plugin-design.md`
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### Known limitation in this alpha
|
||||||
|
- Reliable continuity recovery is currently validated for resident subagents, **not** for Discord main/channel/thread sessions. Fresh Discord tests did not preserve short facts or concrete working-state details across new sessions.
|
||||||
@@ -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.
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
# memory-continuity
|
# memory-continuity
|
||||||
|
|
||||||
OpenClaw skill for **short-term working continuity** — so an agent can recover
|
**Current release:** `v0.3.0-probe`
|
||||||
structured in-flight work state after `/new`, reset, gateway interruption,
|
|
||||||
model fallback, or compaction.
|
OpenClaw continuity package for **short-term working continuity** — currently shipped as:
|
||||||
|
- a **skill** (`SKILL.md`) for behavior contract / fallback recovery
|
||||||
|
- a **lifecycle plugin probe** (`plugin/lifecycle-prototype.ts`) for validating the primary runtime path
|
||||||
|
|
||||||
|
Its goal is to let an agent recover structured in-flight work state after `/new`, reset, gateway interruption, model fallback, or compaction.
|
||||||
|
|
||||||
## What problem does this solve?
|
## What problem does this solve?
|
||||||
|
|
||||||
@@ -24,12 +28,22 @@ That is the problem this skill solves.
|
|||||||
|
|
||||||
## Current architecture stance
|
## Current architecture stance
|
||||||
|
|
||||||
This repository now treats the skill as:
|
> **Alpha support boundary (`v0.3.0-probe`)**
|
||||||
- a **behavior contract**
|
>
|
||||||
- a **fallback implementation**
|
> Currently validated:
|
||||||
- a **human-readable protocol** for structured working-state checkpoints
|
> - resident subagent startup continuity
|
||||||
|
>
|
||||||
|
> Not currently supported / not yet validated for reliable recovery:
|
||||||
|
> - Discord main/channel/thread continuity
|
||||||
|
>
|
||||||
|
|
||||||
The planned primary runtime path is a **standard lifecycle plugin** that can
|
This repository should now be understood as a **continuity package**, not just a standalone skill.
|
||||||
|
|
||||||
|
### Included forms
|
||||||
|
- **Skill** = behavior contract / fallback implementation / human-readable protocol
|
||||||
|
- **Lifecycle plugin probe** = current runtime experiment for the primary architecture
|
||||||
|
|
||||||
|
The intended primary runtime path is a **standard lifecycle plugin** that can
|
||||||
improve startup, `/new`, and compaction continuity **without consuming
|
improve startup, `/new`, and compaction continuity **without consuming
|
||||||
OpenClaw’s exclusive `contextEngine` slot**.
|
OpenClaw’s exclusive `contextEngine` slot**.
|
||||||
|
|
||||||
@@ -147,9 +161,12 @@ memory-continuity/
|
|||||||
├── SKILL.md
|
├── SKILL.md
|
||||||
├── README.md
|
├── README.md
|
||||||
├── LICENSE
|
├── LICENSE
|
||||||
|
├── plugin/
|
||||||
|
│ └── lifecycle-prototype.ts # Phase 2 probe / not production yet
|
||||||
├── references/
|
├── references/
|
||||||
│ ├── template.md
|
│ ├── template.md
|
||||||
│ └── doctor-spec.md
|
│ ├── doctor-spec.md
|
||||||
|
│ └── phase2-hook-validation.md
|
||||||
└── scripts/
|
└── scripts/
|
||||||
└── continuity_doctor.py
|
└── continuity_doctor.py
|
||||||
```
|
```
|
||||||
@@ -181,15 +198,20 @@ Strengthen the current skill version:
|
|||||||
- improve doctor and docs
|
- improve doctor and docs
|
||||||
|
|
||||||
### Phase 2
|
### Phase 2
|
||||||
Build a **standard lifecycle plugin** as the primary runtime path:
|
Build and validate a **standard lifecycle plugin** as the primary runtime path:
|
||||||
- startup recovery behavior
|
- startup recovery behavior
|
||||||
- `/new` checkpointing
|
- `/new` checkpointing
|
||||||
- compaction-boundary checkpointing
|
- compaction-boundary checkpointing
|
||||||
- end-of-run safety writes
|
- end-of-run safety writes
|
||||||
|
- hook validation in real resident subagent sessions
|
||||||
|
|
||||||
### Future option
|
### Future option
|
||||||
Evaluate a ContextEngine variant later only if the slot tradeoff is justified.
|
Evaluate a ContextEngine variant later only if the slot tradeoff is justified.
|
||||||
|
|
||||||
|
## Release notes
|
||||||
|
|
||||||
|
See `CHANGELOG.md` for the current packaged milestone history.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const PLACEHOLDER_VALUES = new Set([
|
||||||
|
"",
|
||||||
|
"none",
|
||||||
|
"n/a",
|
||||||
|
"na",
|
||||||
|
"idle",
|
||||||
|
"[one sentence: what are we trying to accomplish]",
|
||||||
|
"[exactly what should happen next]",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function resolveStatePath(runtime: { workspaceDir?: string }) {
|
||||||
|
const workspaceDir = runtime?.workspaceDir;
|
||||||
|
if (!workspaceDir) return null;
|
||||||
|
return path.join(workspaceDir, "memory", "CURRENT_STATE.md");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStateFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(filePath, "utf8");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSection(markdown: string, heading: string) {
|
||||||
|
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const re = new RegExp(`^##\\s+${escapedHeading}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "m");
|
||||||
|
const m = markdown.match(re);
|
||||||
|
return (m?.[1] ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeValue(value: string) {
|
||||||
|
return value.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMeaningful(value: string) {
|
||||||
|
return !PLACEHOLDER_VALUES.has(normalizeValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSnapshot(markdown: string) {
|
||||||
|
const objective = extractSection(markdown, "Objective");
|
||||||
|
const currentStep = extractSection(markdown, "Current Step");
|
||||||
|
const keyDecisions = extractSection(markdown, "Key Decisions");
|
||||||
|
const nextAction = extractSection(markdown, "Next Action");
|
||||||
|
const blockers = extractSection(markdown, "Blockers");
|
||||||
|
const unsurfacedResults = extractSection(markdown, "Unsurfaced Results");
|
||||||
|
const updated = markdown.match(/^> Last updated:\s*(.+)$/m)?.[1]?.trim() ?? "unknown";
|
||||||
|
|
||||||
|
if (!isMeaningful(objective)) return null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
"CONTINUITY SNAPSHOT",
|
||||||
|
`Objective: ${objective}`,
|
||||||
|
`Current Step: ${currentStep || "unknown"}`,
|
||||||
|
`Key Decisions: ${keyDecisions || "None"}`,
|
||||||
|
`Next Action: ${nextAction || "unknown"}`,
|
||||||
|
`Blockers: ${blockers || "None"}`,
|
||||||
|
`Unsurfaced Results: ${unsurfacedResults || "None"}`,
|
||||||
|
`Updated: ${updated}`,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStateFile(filePath: string) {
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
if (fs.existsSync(filePath)) return;
|
||||||
|
fs.writeFileSync(
|
||||||
|
filePath,
|
||||||
|
`# Current State\n> Last updated: ${new Date().toISOString()}\n\n## Objective\nNone\n\n## Current Step\nNone\n\n## Key Decisions\n- None\n\n## Next Action\nNone\n\n## Blockers\nNone\n\n## Unsurfaced Results\nNone\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatArchiveStamp(date = new Date()) {
|
||||||
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
|
return [
|
||||||
|
date.getFullYear(),
|
||||||
|
pad(date.getMonth() + 1),
|
||||||
|
pad(date.getDate()),
|
||||||
|
].join("-") + `_${pad(date.getHours())}-${pad(date.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendArchive(workspaceDir: string, markdown: string) {
|
||||||
|
const stamp = formatArchiveStamp();
|
||||||
|
const archiveDir = path.join(workspaceDir, "memory", "session_archive");
|
||||||
|
fs.mkdirSync(archiveDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(archiveDir, `${stamp}.md`), markdown, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function register(api: any) {
|
||||||
|
// Startup recovery hint: this is the most important Phase 2 check.
|
||||||
|
api.on(
|
||||||
|
"before_prompt_build",
|
||||||
|
async (_event: any, ctx: any) => {
|
||||||
|
const statePath = resolveStatePath(api.runtime ?? {});
|
||||||
|
if (!statePath) return;
|
||||||
|
const markdown = readStateFile(statePath);
|
||||||
|
if (!markdown) return;
|
||||||
|
const snapshot = buildSnapshot(markdown);
|
||||||
|
if (!snapshot) return;
|
||||||
|
return {
|
||||||
|
prependSystemContext:
|
||||||
|
`${snapshot}\n\n` +
|
||||||
|
"If the user is clearly resuming/recovering prior work, surface the recovered state before generic greeting.",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ priority: 20 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// /new boundary: best-effort archive/checkpoint.
|
||||||
|
api.registerHook(
|
||||||
|
"command:new",
|
||||||
|
async () => {
|
||||||
|
const workspaceDir = api.runtime?.workspaceDir;
|
||||||
|
const statePath = resolveStatePath(api.runtime ?? {});
|
||||||
|
if (!workspaceDir || !statePath) return;
|
||||||
|
ensureStateFile(statePath);
|
||||||
|
const markdown = readStateFile(statePath);
|
||||||
|
if (!markdown) return;
|
||||||
|
appendArchive(workspaceDir, markdown);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "memory-continuity.command-new",
|
||||||
|
description: "Archive CURRENT_STATE.md before /new resets conversational continuity.",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// End-of-run checkpoint stub: proves the lifecycle hook wiring exists.
|
||||||
|
api.on("agent_end", async () => {
|
||||||
|
const statePath = resolveStatePath(api.runtime ?? {});
|
||||||
|
if (!statePath) return;
|
||||||
|
ensureStateFile(statePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compaction hook probe: v1 must confirm this path is actually synchronous enough.
|
||||||
|
api.on("before_compaction", async () => {
|
||||||
|
const statePath = resolveStatePath(api.runtime ?? {});
|
||||||
|
if (!statePath) return;
|
||||||
|
ensureStateFile(statePath);
|
||||||
|
const existing = readStateFile(statePath) ?? "";
|
||||||
|
const marker = `\n<!-- COMPACTION_PROBE ${new Date().toISOString()} -->\n`;
|
||||||
|
if (!existing.includes("<!-- COMPACTION_PROBE ")) {
|
||||||
|
fs.writeFileSync(statePath, existing.trimEnd() + marker, "utf8");
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(
|
||||||
|
statePath,
|
||||||
|
existing.replace(/<!-- COMPACTION_PROBE .*?-->/, marker.trim()),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`[memory-continuity] before_compaction probe wrote marker to ${statePath}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# Phase 2 Hook Validation Notes
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Validate whether a **standard lifecycle plugin** can support the first practical
|
||||||
|
memory-continuity MVP without consuming the exclusive `contextEngine` slot.
|
||||||
|
|
||||||
|
## Current evidence from local docs / SDK
|
||||||
|
|
||||||
|
### Confirmed hook/event names seen in local OpenClaw install
|
||||||
|
From local docs and SDK/runtime sources, the following names are present:
|
||||||
|
|
||||||
|
#### Typed plugin lifecycle hooks (`api.on`)
|
||||||
|
- `before_prompt_build`
|
||||||
|
- `before_agent_start`
|
||||||
|
- `agent_end`
|
||||||
|
- `before_compaction`
|
||||||
|
- `after_compaction`
|
||||||
|
- `subagent_ended`
|
||||||
|
|
||||||
|
#### Event hooks (`api.registerHook`)
|
||||||
|
- `command:new`
|
||||||
|
- `command:reset`
|
||||||
|
- `session:compact:before`
|
||||||
|
- `session:compact:after`
|
||||||
|
- `agent:bootstrap`
|
||||||
|
|
||||||
|
## Important finding: prompt injection is available to standard plugins
|
||||||
|
Local runtime code shows:
|
||||||
|
- `promptInjectionHookNameSet = new Set(["before_prompt_build", "before_agent_start"])`
|
||||||
|
|
||||||
|
This matters because it means a standard plugin may be able to inject startup
|
||||||
|
continuity hints **without** using the exclusive ContextEngine slot.
|
||||||
|
|
||||||
|
## Preferred v1 hook mapping
|
||||||
|
|
||||||
|
### 1. Startup recovery hint
|
||||||
|
**Primary candidate:** `before_prompt_build`
|
||||||
|
|
||||||
|
Why:
|
||||||
|
- explicitly documented as preferred over legacy `before_agent_start`
|
||||||
|
- supports prompt mutation fields such as:
|
||||||
|
- `prependContext`
|
||||||
|
- `prependSystemContext`
|
||||||
|
- `appendSystemContext`
|
||||||
|
- `systemPrompt`
|
||||||
|
- happens after session load, which is more practical for dynamic continuity state
|
||||||
|
|
||||||
|
**Fallback candidate:** `before_agent_start`
|
||||||
|
|
||||||
|
Why:
|
||||||
|
- present in runtime and treated as a prompt injection hook
|
||||||
|
- useful if `before_prompt_build` proves insufficient in some environments
|
||||||
|
|
||||||
|
### 2. `/new` boundary checkpoint
|
||||||
|
**Primary candidate:** `command:new`
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
- save/archive working-state checkpoint before user-triggered reset of conversational continuity
|
||||||
|
|
||||||
|
### 3. End-of-run safety checkpoint
|
||||||
|
**Primary candidate:** `agent_end`
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
- opportunistically preserve checkpoint state even if the agent was imperfectly disciplined mid-turn
|
||||||
|
|
||||||
|
### 4. Compaction protection
|
||||||
|
**Candidates to test:**
|
||||||
|
- `before_compaction`
|
||||||
|
- `session:compact:before`
|
||||||
|
|
||||||
|
This is the most important unresolved path.
|
||||||
|
|
||||||
|
## Biggest unresolved technical risk
|
||||||
|
The lifecycle-plugin route still has one major unsolved problem:
|
||||||
|
|
||||||
|
> How should the plugin expose a startup continuity hint robustly enough to
|
||||||
|
> satisfy “baseline recovery without read”, without relying on ContextEngine-only
|
||||||
|
> `systemPromptAddition`?
|
||||||
|
|
||||||
|
Local evidence suggests `before_prompt_build` may be enough, but this must be
|
||||||
|
validated by a real plugin test.
|
||||||
|
|
||||||
|
## Required Phase 2 experiments
|
||||||
|
|
||||||
|
### Experiment A — startup prompt injection
|
||||||
|
Build a minimal plugin that:
|
||||||
|
- reads `memory/CURRENT_STATE.md`
|
||||||
|
- derives a compact snapshot
|
||||||
|
- injects it through `before_prompt_build`
|
||||||
|
- verifies the snapshot is actually visible to the agent in a fresh run
|
||||||
|
|
||||||
|
### Experiment B — `/new` checkpoint timing
|
||||||
|
Build a minimal plugin that:
|
||||||
|
- handles `command:new`
|
||||||
|
- archives or checkpoints `CURRENT_STATE.md`
|
||||||
|
- verifies the write occurs before continuity is reset
|
||||||
|
|
||||||
|
### Experiment C — compaction boundary guarantee
|
||||||
|
Build a minimal plugin that:
|
||||||
|
- hooks `before_compaction` and/or `session:compact:before`
|
||||||
|
- writes a deterministic marker/checkpoint
|
||||||
|
- verifies compaction waits for hook completion
|
||||||
|
|
||||||
|
**This experiment is mandatory.**
|
||||||
|
If the hook does not reliably block until the write completes, the compaction
|
||||||
|
safety design must be revised.
|
||||||
|
|
||||||
|
### Experiment D — end-of-run safety path
|
||||||
|
Build a minimal plugin that:
|
||||||
|
- hooks `agent_end`
|
||||||
|
- performs a trivial state write
|
||||||
|
- confirms the event fires reliably enough to be useful as a safety net
|
||||||
|
|
||||||
|
## Prototype status
|
||||||
|
A local prototype skeleton exists at:
|
||||||
|
- `plugin/lifecycle-prototype.ts`
|
||||||
|
|
||||||
|
That file is only a Phase 2 probe. It is **not** the final implementation.
|
||||||
|
|
||||||
|
## Current conclusion
|
||||||
|
Based on local documentation and runtime inspection:
|
||||||
|
- a **standard lifecycle plugin** looks viable as the primary v1 architecture
|
||||||
|
- it likely supports startup prompt injection, `/new` handling, run-end checkpointing, and compaction-path interception
|
||||||
|
- the two most important items still requiring live proof are:
|
||||||
|
1. prompt injection quality in `before_prompt_build`
|
||||||
|
2. synchronous compaction safety in `before_compaction` / `session:compact:before`
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
# Memory Continuity Lifecycle Plugin Design
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Design draft only. No plugin implementation yet.
|
||||||
|
|
||||||
|
## Current architectural choice
|
||||||
|
Memory continuity should **not** use the `ContextEngine` slot as its primary v1 architecture.
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- `contextEngine` is an **exclusive slot** in OpenClaw
|
||||||
|
- users should not be forced to choose between memory continuity and context engines such as `lossless-claw`
|
||||||
|
- context compression is a broad baseline need; working-state recovery is an additional capability
|
||||||
|
|
||||||
|
Therefore the main path is:
|
||||||
|
- **skill + ordinary lifecycle plugin** as the primary architecture
|
||||||
|
- **ContextEngine integration** kept as a future option, not the default implementation target
|
||||||
|
|
||||||
|
## Why a plugin version exists
|
||||||
|
The current `memory-continuity` skill is useful, but it depends too much on agent cooperation:
|
||||||
|
- the agent must notice recovery conditions
|
||||||
|
- the agent must keep `memory/CURRENT_STATE.md` updated
|
||||||
|
- the agent often benefits from `read`
|
||||||
|
|
||||||
|
A lifecycle plugin gives a runtime-aligned way to improve reliability without modifying OpenClaw core and without consuming the exclusive ContextEngine slot.
|
||||||
|
|
||||||
|
## Product strategy
|
||||||
|
Keep three forms with clear roles:
|
||||||
|
|
||||||
|
### A. Skill version
|
||||||
|
Role:
|
||||||
|
- zero-dependency fallback
|
||||||
|
- behavior contract
|
||||||
|
- template discipline
|
||||||
|
- compatibility with environments that do not install plugins
|
||||||
|
|
||||||
|
### B. Lifecycle plugin version (primary runtime path)
|
||||||
|
Role:
|
||||||
|
- runtime-assisted recovery
|
||||||
|
- automatic checkpointing at key lifecycle points
|
||||||
|
- better startup and `/new` continuity
|
||||||
|
- coexistence with `lossless-claw` and other context engines
|
||||||
|
|
||||||
|
### C. ContextEngine version (future option)
|
||||||
|
Role:
|
||||||
|
- more powerful prompt-time snapshot injection via `assemble` / `systemPromptAddition`
|
||||||
|
- only worth pursuing later if slot tradeoffs are acceptable or composite engine support exists
|
||||||
|
|
||||||
|
These forms should complement each other, not compete.
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
Primary durable checkpoint file:
|
||||||
|
- `memory/CURRENT_STATE.md`
|
||||||
|
|
||||||
|
The plugin should treat this file as the editable, human-readable source of truth for working state.
|
||||||
|
|
||||||
|
The plugin may derive a lighter runtime snapshot from it, but should not replace it with an opaque database-first design.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
The plugin version should **not**:
|
||||||
|
- replace `MEMORY.md`
|
||||||
|
- replace daily notes in `memory/YYYY-MM-DD.md`
|
||||||
|
- replace native OpenClaw compaction summaries
|
||||||
|
- replace native `memoryFlush`
|
||||||
|
- replace session transcript memory search
|
||||||
|
- persist a full transcript mirror
|
||||||
|
- inject large recovery payloads into every turn
|
||||||
|
- attempt real-time bidirectional state synchronization in v1
|
||||||
|
|
||||||
|
## Core runtime idea
|
||||||
|
Use standard lifecycle hooks to ensure that short-term working state remains available across:
|
||||||
|
- `/new`
|
||||||
|
- reset/restart
|
||||||
|
- compaction
|
||||||
|
- session end / restart-like boundaries
|
||||||
|
- limited subagent handoff scenarios when supported by available hooks
|
||||||
|
|
||||||
|
The plugin should prefer deterministic, structured recovery over free-form recollection.
|
||||||
|
|
||||||
|
## Desired user-visible property
|
||||||
|
Even if an agent lacks `read`, the session should still recover a compact continuity hint whenever there is meaningful active work to recover.
|
||||||
|
|
||||||
|
## Checkpoint schema
|
||||||
|
The checkpoint file should retain a stable, minimal structure:
|
||||||
|
|
||||||
|
```md
|
||||||
|
# Current State
|
||||||
|
> Last updated: 2026-03-12T21:00:00+10:00
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
...
|
||||||
|
|
||||||
|
## Current Step
|
||||||
|
...
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Next Action
|
||||||
|
...
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
None
|
||||||
|
|
||||||
|
## Unsurfaced Results
|
||||||
|
None
|
||||||
|
```
|
||||||
|
|
||||||
|
Potential future additions if truly needed:
|
||||||
|
- `Confidence`
|
||||||
|
- `Freshness`
|
||||||
|
- `Recent Finished`
|
||||||
|
|
||||||
|
But the default should stay compact.
|
||||||
|
|
||||||
|
## Runtime snapshot shape
|
||||||
|
The plugin should derive a much smaller summary than the raw checkpoint file.
|
||||||
|
|
||||||
|
Target content:
|
||||||
|
- Objective
|
||||||
|
- Current Step
|
||||||
|
- Last Confirmed Result or Key Decision(s)
|
||||||
|
- Next Action
|
||||||
|
- Blockers
|
||||||
|
- Unsurfaced Results
|
||||||
|
- Freshness / Updated At
|
||||||
|
|
||||||
|
### Draft example
|
||||||
|
```text
|
||||||
|
CONTINUITY SNAPSHOT
|
||||||
|
Objective: Verify memory continuity for Telegram subagents.
|
||||||
|
Current Step: Tools fixed; validating plugin-backed recovery design.
|
||||||
|
Key Decision: Use lifecycle plugin as primary path; ContextEngine stays optional.
|
||||||
|
Next Action: Finalize hook mapping and update the skill docs.
|
||||||
|
Blockers: None.
|
||||||
|
Unsurfaced Results: None.
|
||||||
|
Updated: 2026-03-12T22:00:00+10:00
|
||||||
|
```
|
||||||
|
|
||||||
|
### Size target
|
||||||
|
- preferred: ~150-300 tokens
|
||||||
|
- avoid large raw checkpoint injection on every turn
|
||||||
|
- skip injection entirely when there is no meaningful active state
|
||||||
|
|
||||||
|
### V1 rule for “meaningful active work”
|
||||||
|
Treat work as active when:
|
||||||
|
- `Objective` is non-empty
|
||||||
|
- and `Objective` is not placeholder text such as `None`, `idle`, `n/a`, or an empty template marker
|
||||||
|
|
||||||
|
This rule can be refined later, but v1 should use a simple deterministic threshold.
|
||||||
|
|
||||||
|
## Primary lifecycle hook mapping
|
||||||
|
|
||||||
|
### 1. Startup hook (`before_agent_start` / closest available startup hook)
|
||||||
|
Purpose:
|
||||||
|
- establish whether recovery state exists
|
||||||
|
- load a compact continuity summary for startup recovery behavior
|
||||||
|
- ensure recovery can happen even when the agent does not explicitly call `read`
|
||||||
|
|
||||||
|
Should do:
|
||||||
|
- check for `memory/CURRENT_STATE.md`
|
||||||
|
- perform lightweight validation
|
||||||
|
- derive a compact startup continuity hint when active work exists
|
||||||
|
- make recovery state available through the startup/lifecycle hook path supported by OpenClaw
|
||||||
|
|
||||||
|
Should avoid:
|
||||||
|
- rewriting the checkpoint unnecessarily
|
||||||
|
- injecting large raw file content
|
||||||
|
- treating placeholder/idle state as active recovery material
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- exact injection mechanism depends on the standard plugin hook surface available in OpenClaw
|
||||||
|
- this design intentionally does **not** assume access to ContextEngine-only `systemPromptAddition`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `/new` hook (`command:new` or equivalent)
|
||||||
|
Purpose:
|
||||||
|
- create a reliable checkpoint right before the user deliberately resets conversational continuity
|
||||||
|
|
||||||
|
Should do:
|
||||||
|
- save a final overwrite-style checkpoint before reset
|
||||||
|
- optionally archive the outgoing checkpoint if that remains part of the skill design
|
||||||
|
- ensure the next session can recover active work from a deterministic file state
|
||||||
|
|
||||||
|
Should avoid:
|
||||||
|
- expensive archival behavior for trivial idle sessions
|
||||||
|
- losing unsurfaced results at reset boundaries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Session end / run-end hook (`agent_end` or closest available end hook)
|
||||||
|
Purpose:
|
||||||
|
- checkpoint work at natural lifecycle boundaries
|
||||||
|
|
||||||
|
Should do:
|
||||||
|
- persist the latest working-state checkpoint when a meaningful state change occurred
|
||||||
|
- preserve unsurfaced results
|
||||||
|
- act as a safety net when the agent followed the protocol imperfectly during the turn
|
||||||
|
|
||||||
|
Should avoid:
|
||||||
|
- noisy writes on obviously trivial/no-op turns
|
||||||
|
- assuming this hook alone is enough for correctness
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Compaction hooks (`session:compact:before` / equivalent)
|
||||||
|
Purpose:
|
||||||
|
- hard safety checkpoint before compaction removes detailed older context
|
||||||
|
|
||||||
|
Should do:
|
||||||
|
- force a final continuity checkpoint before compaction
|
||||||
|
- preserve current objective / step / blockers / unsurfaced results
|
||||||
|
- ensure recovery remains possible after compaction
|
||||||
|
|
||||||
|
Critical requirement:
|
||||||
|
- checkpoint writing in the compaction path must complete **synchronously** before compaction proceeds
|
||||||
|
- if the hook cannot provide that guarantee, this risk must be documented explicitly
|
||||||
|
|
||||||
|
This is the strongest required protection point.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Post-turn maintenance hook (when available)
|
||||||
|
Purpose:
|
||||||
|
- opportunistic checkpoint maintenance after substantive turns
|
||||||
|
|
||||||
|
Should do:
|
||||||
|
- update checkpoint when meaningful state changes are detected
|
||||||
|
- optionally use a configurable cadence (for example every N substantive turns)
|
||||||
|
- keep writes overwrite-oriented rather than append-heavy
|
||||||
|
|
||||||
|
V1 definition of **substantive turn**:
|
||||||
|
A turn counts as substantive when it includes at least one of:
|
||||||
|
- a tool result that materially changes work state
|
||||||
|
- a user confirmation of a decision or direction
|
||||||
|
- an agent statement that a concrete step was completed
|
||||||
|
- a newly discovered blocker or newly surfaced result
|
||||||
|
|
||||||
|
Non-substantive examples:
|
||||||
|
- greetings
|
||||||
|
- acknowledgements
|
||||||
|
- short clarifications without state change
|
||||||
|
- filler chatter
|
||||||
|
|
||||||
|
Should avoid:
|
||||||
|
- writing on every trivial turn
|
||||||
|
- producing noisy I/O for idle chat
|
||||||
|
- becoming the only write path
|
||||||
|
|
||||||
|
Practical stance:
|
||||||
|
- post-turn maintenance is useful, but not sufficient alone
|
||||||
|
- correctness should not rely entirely on semantic heuristics
|
||||||
|
|
||||||
|
## Subagent continuity stance for v1
|
||||||
|
V1 should stay conservative.
|
||||||
|
|
||||||
|
### Allowed in v1
|
||||||
|
- parent → child: minimal seed/handoff when a suitable hook/path exists
|
||||||
|
- child → parent: limited recovery of `Unsurfaced Results`
|
||||||
|
|
||||||
|
### Explicitly out of scope in v1
|
||||||
|
- continuous bidirectional synchronization
|
||||||
|
- real-time merge of parent and child working state
|
||||||
|
- multi-worker consensus state
|
||||||
|
|
||||||
|
This keeps the first implementation tractable and reduces process risk.
|
||||||
|
|
||||||
|
## Interaction with native OpenClaw systems
|
||||||
|
|
||||||
|
### With `memoryFlush`
|
||||||
|
Native `memoryFlush` helps the model store durable memory before compaction.
|
||||||
|
|
||||||
|
Memory continuity should not replace that.
|
||||||
|
Instead:
|
||||||
|
- `memoryFlush` handles durable notes / memory files
|
||||||
|
- continuity handles structured working-state checkpointing
|
||||||
|
|
||||||
|
### With native compaction continuity
|
||||||
|
OpenClaw’s compaction keeps summary information in session history.
|
||||||
|
|
||||||
|
Memory continuity should complement that by providing:
|
||||||
|
- a fixed schema
|
||||||
|
- a stable recovery surface
|
||||||
|
- explicit next-step / blocker / unsurfaced-result fields
|
||||||
|
|
||||||
|
### With session transcript memory search
|
||||||
|
Session memory search can help retrieve prior conversational material.
|
||||||
|
|
||||||
|
Memory continuity is different:
|
||||||
|
- memory search helps answer “what did we discuss?”
|
||||||
|
- continuity helps answer “what were we doing, and what should happen next?”
|
||||||
|
|
||||||
|
### With tools like `read`
|
||||||
|
`read` remains valuable for enhanced recovery and debugging.
|
||||||
|
|
||||||
|
But baseline continuity should not depend on `read` once the lifecycle plugin can expose recovery state through startup/runtime hooks.
|
||||||
|
|
||||||
|
### With ContextEngine plugins such as `lossless-claw`
|
||||||
|
This is the main architectural reason the lifecycle-plugin path is preferred.
|
||||||
|
|
||||||
|
Because `contextEngine` is an exclusive slot, making memory continuity a ContextEngine by default would force users to choose between:
|
||||||
|
- context compression / context assembly plugins
|
||||||
|
- working-state continuity
|
||||||
|
|
||||||
|
V1 should avoid creating that conflict.
|
||||||
|
|
||||||
|
## Open design questions
|
||||||
|
1. Which exact standard hook surface is best for startup recovery injection on current OpenClaw releases?
|
||||||
|
2. How should stale checkpoints be detected and labeled?
|
||||||
|
3. Should the plugin compute confidence/freshness automatically?
|
||||||
|
4. How should the plugin expose a startup continuity hint without relying on ContextEngine-only `systemPromptAddition`?
|
||||||
|
5. Should plugin writes go directly to `memory/CURRENT_STATE.md`, or stage then atomically replace?
|
||||||
|
6. How should compaction-hook guarantees be validated in practice?
|
||||||
|
7. What is the safest minimal parent/child handoff path under current OpenClaw hook support?
|
||||||
|
8. Under what future conditions would a ContextEngine variant become worth the slot tradeoff?
|
||||||
|
|
||||||
|
## Recommended implementation phases
|
||||||
|
|
||||||
|
### Phase 1 — Design + discipline hardening
|
||||||
|
- refine skill documentation
|
||||||
|
- stabilize checkpoint template
|
||||||
|
- clarify scope vs non-goals
|
||||||
|
- improve validation / doctor behavior
|
||||||
|
|
||||||
|
### Phase 2 — Minimal lifecycle plugin MVP
|
||||||
|
- register a standard plugin
|
||||||
|
- implement startup recovery hook behavior
|
||||||
|
- implement `/new` checkpoint behavior
|
||||||
|
- implement end-of-run checkpoint behavior
|
||||||
|
- implement compaction-path checkpoint behavior if the hook guarantees are sufficient
|
||||||
|
|
||||||
|
### Phase 3 — Reliability improvements
|
||||||
|
- add controlled post-turn checkpointing
|
||||||
|
- add freshness/confidence labeling
|
||||||
|
- improve stale-state handling
|
||||||
|
- tune snapshot length and injection behavior
|
||||||
|
|
||||||
|
### Phase 4 — Conservative subagent support
|
||||||
|
- add minimal parent → child seed behavior when safe
|
||||||
|
- add conservative child → parent unsurfaced-result recovery
|
||||||
|
- validate handoff behavior in real workflows
|
||||||
|
|
||||||
|
### Phase 5 — Future option evaluation
|
||||||
|
- reassess whether a ContextEngine variant is worth building
|
||||||
|
- only pursue if slot tradeoffs are acceptable or composite-engine support exists
|
||||||
|
|
||||||
|
## Success criteria
|
||||||
|
The plugin version is successful when:
|
||||||
|
- reset/new sessions recover active work without depending on `read`
|
||||||
|
- compaction no longer destroys actionable in-flight state
|
||||||
|
- the agent does not lose unsurfaced results at reset-like boundaries
|
||||||
|
- the runtime path coexists with `lossless-claw` and similar context engines
|
||||||
|
- startup recovery improves without excessive prompt bloat
|
||||||
|
- behavior aligns with OpenClaw’s official plugin and hook model
|
||||||
|
|
||||||
|
## Short summary
|
||||||
|
The primary long-term implementation should be a **standard lifecycle plugin** that improves continuity without consuming the exclusive ContextEngine slot, while the existing skill remains the **human-readable protocol and fallback behavior contract**. A ContextEngine variant remains a future option, not the default architecture.
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# Memory Continuity Scope
|
||||||
|
|
||||||
|
## One-line definition
|
||||||
|
Memory continuity is a **structured working-state checkpoint** for recovering in-flight work after `/new`, reset, compaction, model fallback, gateway interruption, or subagent handoff.
|
||||||
|
|
||||||
|
## Core goal
|
||||||
|
Preserve just enough short-term state that an agent can answer:
|
||||||
|
- What are we trying to do?
|
||||||
|
- What step were we on?
|
||||||
|
- What was decided?
|
||||||
|
- What should happen next?
|
||||||
|
- What is blocked?
|
||||||
|
- What result exists but has not yet been surfaced?
|
||||||
|
|
||||||
|
This is a recovery layer for **active work**, not a general memory system.
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
The canonical working-state record is a Markdown checkpoint file:
|
||||||
|
- `memory/CURRENT_STATE.md` in the current skill version
|
||||||
|
|
||||||
|
Longer-term direction:
|
||||||
|
- keep the checkpoint file as source of truth
|
||||||
|
- add runtime-assisted continuity delivery derived from that file
|
||||||
|
- keep the file readable/editable by humans and agents
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
Memory continuity **is responsible for**:
|
||||||
|
1. Maintaining a compact, overwrite-oriented checkpoint for current work
|
||||||
|
2. Recovering in-flight work across session breaks
|
||||||
|
3. Preserving active task state through compaction and subagent handoff
|
||||||
|
4. Providing a deterministic place to look for next-step recovery
|
||||||
|
5. Surfacing unsent / unsurfaced results that would otherwise be lost
|
||||||
|
6. Giving agents a standard structure for short-term state updates
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
Memory continuity is **not responsible for**:
|
||||||
|
1. Long-term personal memory curation
|
||||||
|
2. Replacing `MEMORY.md` or daily notes
|
||||||
|
3. Replacing OpenClaw compaction summaries
|
||||||
|
4. Replacing OpenClaw `memoryFlush`
|
||||||
|
5. Replacing session transcript memory search
|
||||||
|
6. Acting as a project-management database
|
||||||
|
7. Acting as a full conversation transcript
|
||||||
|
8. Storing every detail of recent chat history
|
||||||
|
9. Guaranteeing perfect semantic recall of arbitrary facts from all prior turns
|
||||||
|
|
||||||
|
## Relationship to native OpenClaw systems
|
||||||
|
### Native OpenClaw handles
|
||||||
|
- bootstrap/system prompt assembly
|
||||||
|
- compaction lifecycle
|
||||||
|
- memory flush before compaction
|
||||||
|
- transcript persistence
|
||||||
|
- tools, sessions, and runtime orchestration
|
||||||
|
- context engine selection and plugin lifecycle
|
||||||
|
- session transcript recall via session-aware memory search
|
||||||
|
|
||||||
|
### Memory continuity adds
|
||||||
|
- a **structured checkpoint** for working state
|
||||||
|
- a predictable recovery format independent of transcript shape
|
||||||
|
- explicit fields for `Objective`, `Current Step`, `Next Action`, `Blockers`, and `Unsurfaced Results`
|
||||||
|
- stronger short-term recovery for in-flight work than generic compaction summaries alone
|
||||||
|
|
||||||
|
### Boundary with session memory search
|
||||||
|
Session memory search can help answer questions like:
|
||||||
|
- what did we discuss before?
|
||||||
|
- what decision was mentioned in a prior session?
|
||||||
|
|
||||||
|
Memory continuity is for a different question:
|
||||||
|
- what are we doing **right now**, where did we stop, and what should happen next?
|
||||||
|
|
||||||
|
In short:
|
||||||
|
- session memory search is good at **recalling prior conversation material**
|
||||||
|
- memory continuity is good at **recovering active working state**
|
||||||
|
|
||||||
|
## Product forms
|
||||||
|
### 1. Skill version (current / fallback version)
|
||||||
|
Purpose:
|
||||||
|
- zero-dependency compatibility layer
|
||||||
|
- human-readable protocol for agents
|
||||||
|
- works today without plugin installation
|
||||||
|
|
||||||
|
What it should do:
|
||||||
|
- define update discipline
|
||||||
|
- define recovery behavior
|
||||||
|
- define template shape
|
||||||
|
- define failure/uncertainty handling
|
||||||
|
|
||||||
|
What it cannot guarantee:
|
||||||
|
- recovery without agent cooperation
|
||||||
|
- recovery without correct tool/config support
|
||||||
|
- automatic runtime injection on every turn
|
||||||
|
|
||||||
|
### 2. Lifecycle plugin version (target architecture)
|
||||||
|
Purpose:
|
||||||
|
- runtime-assisted continuity guarantees without taking the exclusive ContextEngine slot
|
||||||
|
- reduced dependence on `read`
|
||||||
|
- better startup, `/new`, and compaction continuity
|
||||||
|
- coexistence with context engines such as `lossless-claw`
|
||||||
|
|
||||||
|
What it should do:
|
||||||
|
- use ordinary lifecycle hooks to checkpoint and recover working state
|
||||||
|
- improve startup recovery behavior
|
||||||
|
- checkpoint before destructive context transitions when hooks permit it
|
||||||
|
- support minimal parent/child continuity handoff without requiring full bidirectional sync
|
||||||
|
|
||||||
|
### 3. ContextEngine version (future option, not v1)
|
||||||
|
Purpose:
|
||||||
|
- more powerful prompt-time continuity injection when the ecosystem tradeoff is worth it
|
||||||
|
|
||||||
|
Why it is not the current primary path:
|
||||||
|
- `contextEngine` is an exclusive plugin slot
|
||||||
|
- users should not be forced to choose between memory continuity and widely useful context engines such as `lossless-claw`
|
||||||
|
|
||||||
|
## Design principles
|
||||||
|
1. **Files remain source of truth**
|
||||||
|
2. **Structured checkpoint beats free-form summary**
|
||||||
|
3. **Recovery state must stay short**
|
||||||
|
4. **Read access is an enhancement, not the only path**
|
||||||
|
5. **Continuity complements native OpenClaw memory; it does not replace it**
|
||||||
|
6. **Working-state recovery must prefer truth over confident guessing**
|
||||||
|
7. **User-visible recovery should prioritize current task state over generic greetings when continuity is clearly requested**
|
||||||
|
8. **Ecosystem compatibility matters: continuity should not unnecessarily block other high-value plugins**
|
||||||
|
|
||||||
|
## Minimal recovery fields
|
||||||
|
Any continuity implementation should preserve, at minimum:
|
||||||
|
- Objective
|
||||||
|
- Current Step
|
||||||
|
- Key Decisions / Key Facts
|
||||||
|
- Next Action
|
||||||
|
- Blockers
|
||||||
|
- Unsurfaced Results
|
||||||
|
- Updated At / Freshness
|
||||||
|
|
||||||
|
## Success criteria
|
||||||
|
A good continuity implementation should let an agent recover:
|
||||||
|
- the current objective
|
||||||
|
- the latest confirmed step
|
||||||
|
- the next concrete action
|
||||||
|
- the main blocker, if any
|
||||||
|
- one or more unsurfaced results
|
||||||
|
|
||||||
|
Even after:
|
||||||
|
- `/new`
|
||||||
|
- session reset
|
||||||
|
- compaction
|
||||||
|
- subagent handoff
|
||||||
|
- gateway interruption
|
||||||
|
|
||||||
|
## Failure criteria
|
||||||
|
The continuity layer is considered insufficient if, after a reset-like event, the agent:
|
||||||
|
- forgets the active objective
|
||||||
|
- loses a confirmed decision
|
||||||
|
- cannot identify the next action
|
||||||
|
- hides completed but unsurfaced results
|
||||||
|
- hallucinates prior work instead of expressing uncertainty
|
||||||
|
- when `CURRENT_STATE.md` exists and contains active work, opens with generic greeting/chit-chat instead of first surfacing the recovered state in a recovery scenario
|
||||||
|
|
||||||
|
## Current roadmap stance
|
||||||
|
- **Short term:** strengthen the existing skill + file discipline version
|
||||||
|
- **Medium term:** implement a standard lifecycle plugin version aligned with OpenClaw’s official hook model
|
||||||
|
- **Long term:** keep multiple compatible forms
|
||||||
|
- skill = fallback + behavior contract
|
||||||
|
- lifecycle plugin = primary runtime-assisted reliability layer
|
||||||
|
- context-engine variant = optional future path when slot tradeoffs are acceptable
|
||||||
Executable → Regular
+212
-119
@@ -1,143 +1,236 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import annotations
|
"""
|
||||||
|
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 argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REQUIRED_SECTIONS = [
|
|
||||||
'## In Flight',
|
|
||||||
'## Blocked / Waiting',
|
|
||||||
'## Recently Finished',
|
|
||||||
'## Next',
|
|
||||||
'## Reset Summary',
|
|
||||||
]
|
|
||||||
|
|
||||||
AGENTS_MARKERS = [
|
# ---------------------------------------------------------------------------
|
||||||
'CURRENT_STATE.md - Your Short-Term Workbench',
|
# Severity levels
|
||||||
'Dual reporting protocol',
|
# ---------------------------------------------------------------------------
|
||||||
'Execution agent → main',
|
class Severity:
|
||||||
'main → Tao',
|
OK = "OK"
|
||||||
]
|
INFO = "INFO"
|
||||||
|
WARNING = "WARNING"
|
||||||
|
CRITICAL = "CRITICAL"
|
||||||
|
|
||||||
|
|
||||||
def result(level: str, msg: str) -> tuple[str, str]:
|
# ---------------------------------------------------------------------------
|
||||||
print(f'{level:<5} {msg}')
|
# Collector
|
||||||
return level, msg
|
# ---------------------------------------------------------------------------
|
||||||
|
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 line_count(path: Path) -> int:
|
def print_report(self):
|
||||||
try:
|
for severity, message in self.entries:
|
||||||
return len(path.read_text().splitlines())
|
tag = f"[{severity}]".ljust(12)
|
||||||
except Exception:
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def has_sections(path: Path) -> list[str]:
|
# ---------------------------------------------------------------------------
|
||||||
text = path.read_text()
|
# Required sections in CURRENT_STATE.md
|
||||||
missing = [s for s in REQUIRED_SECTIONS if s not in text]
|
# ---------------------------------------------------------------------------
|
||||||
return missing
|
REQUIRED_SECTIONS = [
|
||||||
|
"Objective",
|
||||||
|
"Current Step",
|
||||||
|
"Key Decisions",
|
||||||
|
"Next Action",
|
||||||
|
"Blockers",
|
||||||
|
"Unsurfaced Results",
|
||||||
|
]
|
||||||
|
|
||||||
|
PLACEHOLDER_PATTERNS = [
|
||||||
|
r"\[.*?\]", # anything in square brackets like [One sentence: ...]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
# ---------------------------------------------------------------------------
|
||||||
ap = argparse.ArgumentParser(description='Check continuity/CURRENT_STATE coverage and drift.')
|
# Checks
|
||||||
ap.add_argument('--main-workspace', required=True)
|
# ---------------------------------------------------------------------------
|
||||||
ap.add_argument('--agents-root', required=True)
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
main_ws = Path(args.main_workspace)
|
def check_existence(workspace: Path, report: DiagnosticReport) -> Path | None:
|
||||||
agents_root = Path(args.agents_root)
|
"""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
|
||||||
|
|
||||||
failures = 0
|
|
||||||
warns = 0
|
|
||||||
|
|
||||||
print('continuity doctor\n')
|
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
|
||||||
|
|
||||||
# main AGENTS.md
|
if age_hours > 24:
|
||||||
agents_md = main_ws / 'AGENTS.md'
|
report.add(
|
||||||
if not agents_md.exists():
|
Severity.WARNING,
|
||||||
failures += 1
|
f"CURRENT_STATE.md is stale (last modified {age_hours:.1f}h ago)",
|
||||||
result('FAIL', f'missing {agents_md}')
|
)
|
||||||
|
elif age_hours > 4:
|
||||||
|
report.add(
|
||||||
|
Severity.INFO,
|
||||||
|
f"CURRENT_STATE.md last modified {age_hours:.1f}h ago",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
text = agents_md.read_text()
|
report.add(Severity.OK, f"CURRENT_STATE.md is fresh ({age_hours:.1f}h old)")
|
||||||
missing = [m for m in AGENTS_MARKERS if m not in text]
|
|
||||||
if missing:
|
|
||||||
failures += 1
|
|
||||||
result('FAIL', f'AGENTS continuity markers missing: {", ".join(missing)}')
|
|
||||||
else:
|
|
||||||
result('PASS', 'AGENTS continuity rules present')
|
|
||||||
|
|
||||||
# main CURRENT_STATE
|
|
||||||
main_cs = main_ws / 'memory' / 'CURRENT_STATE.md'
|
def check_template_compliance(state_file: Path, report: DiagnosticReport) -> dict:
|
||||||
if not main_cs.exists():
|
"""Check all required sections are present and not placeholder-only."""
|
||||||
failures += 1
|
content = state_file.read_text(encoding="utf-8")
|
||||||
result('FAIL', 'main CURRENT_STATE missing')
|
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:
|
else:
|
||||||
missing = has_sections(main_cs)
|
report.add(Severity.OK, "No unsurfaced results pending")
|
||||||
if missing:
|
|
||||||
failures += 1
|
|
||||||
result('FAIL', f'main CURRENT_STATE missing sections: {", ".join(missing)}')
|
|
||||||
else:
|
|
||||||
result('PASS', 'main CURRENT_STATE sections present')
|
|
||||||
n = line_count(main_cs)
|
|
||||||
if n > 50:
|
|
||||||
warns += 1
|
|
||||||
result('WARN', f'main CURRENT_STATE too long: {n} lines (cap 50)')
|
|
||||||
else:
|
|
||||||
result('PASS', f'main CURRENT_STATE length ok: {n} lines')
|
|
||||||
|
|
||||||
# agent workspaces
|
|
||||||
checked = 0
|
|
||||||
missing_files = []
|
|
||||||
missing_sections = []
|
|
||||||
oversize = []
|
|
||||||
|
|
||||||
if agents_root.exists():
|
|
||||||
for ws in sorted([p for p in agents_root.iterdir() if p.is_dir()]):
|
|
||||||
checked += 1
|
|
||||||
cs = ws / 'memory' / 'CURRENT_STATE.md'
|
|
||||||
if not cs.exists():
|
|
||||||
missing_files.append(ws.name)
|
|
||||||
continue
|
|
||||||
missing = has_sections(cs)
|
|
||||||
if missing:
|
|
||||||
missing_sections.append((ws.name, missing))
|
|
||||||
n = line_count(cs)
|
|
||||||
if n > 30:
|
|
||||||
oversize.append((ws.name, n))
|
|
||||||
|
|
||||||
if missing_files:
|
|
||||||
failures += 1
|
|
||||||
result('FAIL', f'agent CURRENT_STATE missing: {", ".join(missing_files)}')
|
|
||||||
else:
|
|
||||||
result('PASS', f'agent CURRENT_STATE files present: {checked}/{checked}')
|
|
||||||
|
|
||||||
if missing_sections:
|
|
||||||
failures += 1
|
|
||||||
details = '; '.join(f'{name}: {", ".join(m)}' for name, m in missing_sections)
|
|
||||||
result('FAIL', f'agent CURRENT_STATE missing sections: {details}')
|
|
||||||
else:
|
|
||||||
result('PASS', 'agent CURRENT_STATE sections present')
|
|
||||||
|
|
||||||
if oversize:
|
|
||||||
warns += 1
|
|
||||||
details = '; '.join(f'{name}: {n} lines' for name, n in oversize)
|
|
||||||
result('WARN', f'agent CURRENT_STATE oversize: {details}')
|
|
||||||
else:
|
|
||||||
result('PASS', 'agent CURRENT_STATE length caps respected')
|
|
||||||
|
|
||||||
print('\nSummary')
|
|
||||||
print(f'- failures: {failures}')
|
|
||||||
print(f'- warnings: {warns}')
|
|
||||||
|
|
||||||
if failures:
|
|
||||||
print('\nSuggested actions:')
|
|
||||||
print('1. create/restore missing CURRENT_STATE files')
|
|
||||||
print('2. restore AGENTS continuity section if missing')
|
|
||||||
print('3. trim oversized CURRENT_STATE files')
|
|
||||||
print('4. rerun continuity doctor')
|
|
||||||
return 2
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def check_archive(workspace: Path, sections: dict, report: DiagnosticReport):
|
||||||
raise SystemExit(main())
|
"""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))
|
||||||
|
|||||||
Reference in New Issue
Block a user