diff --git a/SOUL.md b/SOUL.md new file mode 100644 index 0000000..82ced55 --- /dev/null +++ b/SOUL.md @@ -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._ diff --git a/skills/memory-continuity/CHANGELOG.md b/skills/memory-continuity/CHANGELOG.md new file mode 100644 index 0000000..1b0fc62 --- /dev/null +++ b/skills/memory-continuity/CHANGELOG.md @@ -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. diff --git a/skills/memory-continuity/LICENSE b/skills/memory-continuity/LICENSE new file mode 100644 index 0000000..925d4f0 --- /dev/null +++ b/skills/memory-continuity/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 dtzp555-max + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/memory-continuity/README.md b/skills/memory-continuity/README.md index 3541c88..8f0d5ef 100644 --- a/skills/memory-continuity/README.md +++ b/skills/memory-continuity/README.md @@ -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 diff --git a/skills/memory-continuity/memory/.gitkeep b/skills/memory-continuity/memory/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/memory-continuity/plugin/lifecycle-prototype.ts b/skills/memory-continuity/plugin/lifecycle-prototype.ts new file mode 100644 index 0000000..c4a3796 --- /dev/null +++ b/skills/memory-continuity/plugin/lifecycle-prototype.ts @@ -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\n`; + if (!existing.includes("/, marker.trim()), + "utf8", + ); + } + console.log(`[memory-continuity] before_compaction probe wrote marker to ${statePath}`); + }); +} diff --git a/skills/memory-continuity/references/phase2-hook-validation.md b/skills/memory-continuity/references/phase2-hook-validation.md new file mode 100644 index 0000000..7460ba0 --- /dev/null +++ b/skills/memory-continuity/references/phase2-hook-validation.md @@ -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` diff --git a/skills/memory-continuity/references/plugin-design.md b/skills/memory-continuity/references/plugin-design.md new file mode 100644 index 0000000..e8047bb --- /dev/null +++ b/skills/memory-continuity/references/plugin-design.md @@ -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. diff --git a/skills/memory-continuity/references/scope.md b/skills/memory-continuity/references/scope.md new file mode 100644 index 0000000..7c940e4 --- /dev/null +++ b/skills/memory-continuity/references/scope.md @@ -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 diff --git a/skills/memory-continuity/scripts/continuity_doctor.py b/skills/memory-continuity/scripts/continuity_doctor.py old mode 100755 new mode 100644 index d3bbcc6..9f0ee91 --- a/skills/memory-continuity/scripts/continuity_doctor.py +++ b/skills/memory-continuity/scripts/continuity_doctor.py @@ -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: + def print_report(self): + for severity, message in self.entries: + tag = f"[{severity}]".ljust(12) + print(f"{tag}{message}") + print() + print(f"Overall status: {self._worst}") + + @property + def exit_code(self) -> int: + if self._worst == Severity.CRITICAL: + return 2 + if self._worst == Severity.WARNING: + return 1 return 0 -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 +# --------------------------------------------------------------------------- +# 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: ...] +] -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() +# --------------------------------------------------------------------------- +# Checks +# --------------------------------------------------------------------------- - main_ws = Path(args.main_workspace) - agents_root = Path(args.agents_root) +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 - 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 - agents_md = main_ws / 'AGENTS.md' - if not agents_md.exists(): - failures += 1 - result('FAIL', f'missing {agents_md}') + 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: - 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') + report.add(Severity.OK, f"CURRENT_STATE.md is fresh ({age_hours:.1f}h old)") - # main CURRENT_STATE - main_cs = main_ws / 'memory' / 'CURRENT_STATE.md' - if not main_cs.exists(): - failures += 1 - result('FAIL', 'main CURRENT_STATE missing') + +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: - 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') - return 2 - return 0 + report.add(Severity.OK, "No unsurfaced results pending") -if __name__ == '__main__': - raise SystemExit(main()) +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))