mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-22 05:25:07 +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
|
||||
|
||||
OpenClaw skill for **short-term working continuity** — so an agent can recover
|
||||
structured in-flight work state after `/new`, reset, gateway interruption,
|
||||
model fallback, or compaction.
|
||||
**Current release:** `v0.3.0-probe`
|
||||
|
||||
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?
|
||||
|
||||
@@ -24,12 +28,22 @@ That is the problem this skill solves.
|
||||
|
||||
## Current architecture stance
|
||||
|
||||
This repository now treats the skill as:
|
||||
- a **behavior contract**
|
||||
- a **fallback implementation**
|
||||
- a **human-readable protocol** for structured working-state checkpoints
|
||||
> **Alpha support boundary (`v0.3.0-probe`)**
|
||||
>
|
||||
> Currently validated:
|
||||
> - 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
|
||||
OpenClaw’s exclusive `contextEngine` slot**.
|
||||
|
||||
@@ -147,9 +161,12 @@ memory-continuity/
|
||||
├── SKILL.md
|
||||
├── README.md
|
||||
├── LICENSE
|
||||
├── plugin/
|
||||
│ └── lifecycle-prototype.ts # Phase 2 probe / not production yet
|
||||
├── references/
|
||||
│ ├── template.md
|
||||
│ └── doctor-spec.md
|
||||
│ ├── doctor-spec.md
|
||||
│ └── phase2-hook-validation.md
|
||||
└── scripts/
|
||||
└── continuity_doctor.py
|
||||
```
|
||||
@@ -181,15 +198,20 @@ Strengthen the current skill version:
|
||||
- improve doctor and docs
|
||||
|
||||
### 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
|
||||
- `/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.
|
||||
|
||||
## License
|
||||
|
||||
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
+221
-128
@@ -1,143 +1,236 @@
|
||||
#!/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 os
|
||||
import sys
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
REQUIRED_SECTIONS = [
|
||||
'## In Flight',
|
||||
'## Blocked / Waiting',
|
||||
'## Recently Finished',
|
||||
'## Next',
|
||||
'## Reset Summary',
|
||||
]
|
||||
|
||||
AGENTS_MARKERS = [
|
||||
'CURRENT_STATE.md - Your Short-Term Workbench',
|
||||
'Dual reporting protocol',
|
||||
'Execution agent → main',
|
||||
'main → Tao',
|
||||
]
|
||||
# ---------------------------------------------------------------------------
|
||||
# Severity levels
|
||||
# ---------------------------------------------------------------------------
|
||||
class Severity:
|
||||
OK = "OK"
|
||||
INFO = "INFO"
|
||||
WARNING = "WARNING"
|
||||
CRITICAL = "CRITICAL"
|
||||
|
||||
|
||||
def result(level: str, msg: str) -> tuple[str, str]:
|
||||
print(f'{level:<5} {msg}')
|
||||
return level, msg
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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 line_count(path: Path) -> int:
|
||||
try:
|
||||
return len(path.read_text().splitlines())
|
||||
except Exception:
|
||||
return 0
|
||||
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}")
|
||||
|
||||
|
||||
def has_sections(path: Path) -> list[str]:
|
||||
text = path.read_text()
|
||||
missing = [s for s in REQUIRED_SECTIONS if s not in text]
|
||||
return missing
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description='Check continuity/CURRENT_STATE coverage and drift.')
|
||||
ap.add_argument('--main-workspace', required=True)
|
||||
ap.add_argument('--agents-root', required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
main_ws = Path(args.main_workspace)
|
||||
agents_root = Path(args.agents_root)
|
||||
|
||||
failures = 0
|
||||
warns = 0
|
||||
|
||||
print('continuity doctor\n')
|
||||
|
||||
# main AGENTS.md
|
||||
agents_md = main_ws / 'AGENTS.md'
|
||||
if not agents_md.exists():
|
||||
failures += 1
|
||||
result('FAIL', f'missing {agents_md}')
|
||||
else:
|
||||
text = agents_md.read_text()
|
||||
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'
|
||||
if not main_cs.exists():
|
||||
failures += 1
|
||||
result('FAIL', 'main CURRENT_STATE missing')
|
||||
else:
|
||||
missing = has_sections(main_cs)
|
||||
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')
|
||||
@property
|
||||
def exit_code(self) -> int:
|
||||
if self._worst == Severity.CRITICAL:
|
||||
return 2
|
||||
if self._worst == Severity.WARNING:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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))
|
||||
|
||||
Reference in New Issue
Block a user