mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-19 09:42:42 +00:00
fix: v5.0.1 — critical bug fixes from code review
- C1: Remove regex g flag in cmdSearch (skipped matches due to lastIndex) - C2: Escape user input in RegExp constructor (crash on special chars) - C3: Convert STATE_TEMPLATE to stateTemplate() function (stale timestamp) - C4: Validate ignorePatterns length to prevent ReDoS - W3: Path traversal guard on agentId in resolveAgentWorkspace + discoverAgents - W7: Correct ISO 8601 week number calculation at year boundaries - W8: Fix false tag dedup with section-specific regex extraction - Add context-engine-evaluation.md (decision: hooks-only, no CE variant) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,8 @@ function estimateTokens(text) {
|
|||||||
return Math.ceil(cjkCount * 1.5) + Math.ceil(nonCjkLen / 4);
|
return Math.ceil(cjkCount * 1.5) + Math.ceil(nonCjkLen / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATE_TEMPLATE = `# Current State
|
function stateTemplate() {
|
||||||
|
return `# Current State
|
||||||
> Last updated: ${new Date().toISOString()}
|
> Last updated: ${new Date().toISOString()}
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
@@ -66,6 +67,7 @@ None
|
|||||||
## Unsurfaced Results
|
## Unsurfaced Results
|
||||||
None
|
None
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -389,9 +391,11 @@ function generateWeeklySummary(workspaceDir, config = {}) {
|
|||||||
const prevMonday = new Date(now);
|
const prevMonday = new Date(now);
|
||||||
prevMonday.setDate(prevMonday.getDate() - 7);
|
prevMonday.setDate(prevMonday.getDate() - 7);
|
||||||
|
|
||||||
// ISO week number
|
// Correct ISO 8601 week number
|
||||||
const jan1 = new Date(prevMonday.getFullYear(), 0, 1);
|
const target = new Date(prevMonday.valueOf());
|
||||||
const weekNum = Math.ceil(((prevMonday - jan1) / 86400000 + jan1.getDay() + 1) / 7);
|
target.setDate(target.getDate() + 3 - ((target.getDay() + 6) % 7));
|
||||||
|
const jan4 = new Date(target.getFullYear(), 0, 4);
|
||||||
|
const weekNum = 1 + Math.round(((target - jan4) / 86400000 - 3 + ((jan4.getDay() + 6) % 7)) / 7);
|
||||||
const weekLabel = `${prevMonday.getFullYear()}-W${pad(weekNum)}`;
|
const weekLabel = `${prevMonday.getFullYear()}-W${pad(weekNum)}`;
|
||||||
|
|
||||||
const weeklyDir = path.join(workspaceDir, "memory", "summaries", "weekly");
|
const weeklyDir = path.join(workspaceDir, "memory", "summaries", "weekly");
|
||||||
@@ -483,8 +487,9 @@ function updateTagIndex(workspaceDir, topic, dateStr) {
|
|||||||
|
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
const normalizedTag = tag.toLowerCase();
|
const normalizedTag = tag.toLowerCase();
|
||||||
// Check if this tag+date combo already exists
|
// Check if this exact tag+date combo already exists under this tag's section
|
||||||
if (existing.includes(`${normalizedTag}`) && existing.includes(dateStr)) continue;
|
const tagSection = existing.match(new RegExp(`## ${normalizedTag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n([\\s\\S]*?)(?=\\n## |$)`));
|
||||||
|
if (tagSection && tagSection[1].includes(dateStr)) continue;
|
||||||
|
|
||||||
// Find or create tag section
|
// Find or create tag section
|
||||||
const tagHeader = `## ${normalizedTag}`;
|
const tagHeader = `## ${normalizedTag}`;
|
||||||
@@ -736,6 +741,9 @@ function parseParentAgentId(sessionKey) {
|
|||||||
function resolveAgentWorkspace(agentId) {
|
function resolveAgentWorkspace(agentId) {
|
||||||
if (!agentId) return null;
|
if (!agentId) return null;
|
||||||
|
|
||||||
|
// Prevent path traversal via crafted agent IDs
|
||||||
|
if (!/^[\w-]+$/.test(agentId)) return null;
|
||||||
|
|
||||||
const base = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "/tmp", ".openclaw");
|
const base = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "/tmp", ".openclaw");
|
||||||
|
|
||||||
// Try to read config for explicit workspace mapping
|
// Try to read config for explicit workspace mapping
|
||||||
@@ -942,6 +950,7 @@ const plugin = {
|
|||||||
|
|
||||||
// Check ignore patterns — skip sessions matching cron/subagent noise
|
// Check ignore patterns — skip sessions matching cron/subagent noise
|
||||||
const ignorePatterns = (config.ignorePatterns || [])
|
const ignorePatterns = (config.ignorePatterns || [])
|
||||||
|
.filter(p => typeof p === "string" && p.length <= 100)
|
||||||
.map(p => { try { return new RegExp(p, "i"); } catch { log.warn?.("[memory-continuity] ignorePatterns: invalid regex, skipping: " + p); return null; } })
|
.map(p => { try { return new RegExp(p, "i"); } catch { log.warn?.("[memory-continuity] ignorePatterns: invalid regex, skipping: " + p); return null; } })
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
@@ -986,7 +995,7 @@ const plugin = {
|
|||||||
if (!statePath) return;
|
if (!statePath) return;
|
||||||
|
|
||||||
if (!readFile(statePath)) {
|
if (!readFile(statePath)) {
|
||||||
writeFile(statePath, STATE_TEMPLATE);
|
writeFile(statePath, stateTemplate());
|
||||||
log.info?.("[memory-continuity] Created initial CURRENT_STATE.md");
|
log.info?.("[memory-continuity] Created initial CURRENT_STATE.md");
|
||||||
}
|
}
|
||||||
}, { priority: 90 });
|
}, { priority: 90 });
|
||||||
@@ -1048,7 +1057,7 @@ const plugin = {
|
|||||||
|
|
||||||
let parentMd = readFile(parentStatePath);
|
let parentMd = readFile(parentStatePath);
|
||||||
if (!parentMd) {
|
if (!parentMd) {
|
||||||
parentMd = STATE_TEMPLATE;
|
parentMd = stateTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the recovery note
|
// Build the recovery note
|
||||||
|
|||||||
+3
-1
@@ -26,6 +26,7 @@ function discoverAgents() {
|
|||||||
// Sub-agent workspaces
|
// Sub-agent workspaces
|
||||||
try {
|
try {
|
||||||
for (const d of fs.readdirSync(EXTRA_WS)) {
|
for (const d of fs.readdirSync(EXTRA_WS)) {
|
||||||
|
if (!/^[\w-]+$/.test(d)) continue;
|
||||||
const memDir = path.join(EXTRA_WS, d, "memory");
|
const memDir = path.join(EXTRA_WS, d, "memory");
|
||||||
if (fs.existsSync(path.join(memDir, "CURRENT_STATE.md"))) {
|
if (fs.existsSync(path.join(memDir, "CURRENT_STATE.md"))) {
|
||||||
agents.push({ name: d, memDir });
|
agents.push({ name: d, memDir });
|
||||||
@@ -233,7 +234,8 @@ function cmdSearch(args) {
|
|||||||
agent = null;
|
agent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const re = new RegExp(keyword, "gi");
|
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const re = new RegExp(escaped, "i");
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
const searchAgents = agent ? [{ name: agent, memDir: resolveMemDir(agent) }] : agents;
|
const searchAgents = agent ? [{ name: agent, memDir: resolveMemDir(agent) }] : agents;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "memory-continuity",
|
"id": "memory-continuity",
|
||||||
"name": "Memory Continuity",
|
"name": "Memory Continuity",
|
||||||
"description": "Preserves working state across /new, reset, compaction, and gateway restarts via a simple markdown checkpoint file.",
|
"description": "Preserves working state across /new, reset, compaction, and gateway restarts via a simple markdown checkpoint file.",
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"author": { "name": "dtzp555-max", "url": "https://github.com/dtzp555-max" },
|
"author": { "name": "dtzp555-max", "url": "https://github.com/dtzp555-max" },
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "https://github.com/dtzp555-max/memory-continuity",
|
"repository": "https://github.com/dtzp555-max/memory-continuity",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "memory-continuity",
|
"name": "memory-continuity",
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"description": "Zero-dependency memory continuity for OpenClaw — plain markdown, lifecycle hooks, no vector DB.",
|
"description": "Zero-dependency memory continuity for OpenClaw — plain markdown, lifecycle hooks, no vector DB.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# ContextEngine Variant Evaluation
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Decision document. Phase 5 evaluation as outlined in plugin-design.md.
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**Decision: Do not build a ContextEngine variant.**
|
||||||
|
|
||||||
|
Memory Continuity already achieves its core recovery goals through lifecycle hooks and `before_prompt_build`. The contextEngine slot is exclusive, and occupying it would break coexistence with lossless-claw and future context engines -- destroying MC's strongest ecosystem advantage. The marginal gains from `assemble()` and `systemPromptAddition` do not justify the slot cost, especially since `prependSystemContext` via `before_prompt_build` already provides prompt-time injection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What contextEngine provides vs current hooks approach
|
||||||
|
|
||||||
|
### ContextEngine slot capabilities (theoretical)
|
||||||
|
| Capability | Description |
|
||||||
|
|---|---|
|
||||||
|
| `assemble()` | Full control over context assembly -- decides what goes into the prompt, in what order, with what priority |
|
||||||
|
| `systemPromptAddition` | Guaranteed system prompt injection with engine-level priority |
|
||||||
|
| Context window management | Direct control over token budgets, message pruning, and compression strategy |
|
||||||
|
| Turn-level interception | Can modify or rewrite every message before it reaches the model |
|
||||||
|
|
||||||
|
### What MC currently uses (hooks-only)
|
||||||
|
| Capability | Implementation |
|
||||||
|
|---|---|
|
||||||
|
| `before_prompt_build` | Injects continuity snapshot via `prependSystemContext` -- works today, confirmed in phase 2 validation |
|
||||||
|
| `command:new` | Archives checkpoint before `/new` reset |
|
||||||
|
| `agent_end` | Safety checkpoint at session end |
|
||||||
|
| `before_compaction` | Protection checkpoint before compaction |
|
||||||
|
| `subagent_ended` | Child-to-parent result recovery |
|
||||||
|
| Session logging, summaries, relevance injection | All implemented via hooks without needing the engine slot |
|
||||||
|
|
||||||
|
### Gap analysis: what would a ContextEngine variant actually gain?
|
||||||
|
|
||||||
|
1. **`assemble()` -- full context control**: MC does not need this. MC's job is to inject a 150-300 token snapshot at startup and protect state at boundaries. It does not need to control the entire context assembly pipeline. That is a context compression concern (lossless-claw's domain).
|
||||||
|
|
||||||
|
2. **`systemPromptAddition`**: MC already achieves equivalent functionality via `prependSystemContext` in the `before_prompt_build` hook. Phase 2 validation confirmed this works. The injection is not ContextEngine-exclusive.
|
||||||
|
|
||||||
|
3. **Token budget management**: MC's snapshot is deliberately small (~150-300 tokens). It does not need fine-grained token budget control. Oversized state files are handled by the `maxStateLines` config and `mc compact` command.
|
||||||
|
|
||||||
|
4. **Turn-level interception**: MC does not need to modify arbitrary turns. Its concern is boundary events (startup, /new, compaction, session end), all of which are already covered by lifecycle hooks.
|
||||||
|
|
||||||
|
**Conclusion**: The practical capabilities MC needs are already available through the hooks API. The additional capabilities from the contextEngine slot solve problems MC does not have.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slot conflict analysis
|
||||||
|
|
||||||
|
### The core constraint
|
||||||
|
OpenClaw's `contextEngine` is an **exclusive slot** -- only one plugin can occupy it at a time.
|
||||||
|
|
||||||
|
### Impact on lossless-claw coexistence
|
||||||
|
MC's `openclaw.plugin.json` explicitly declares:
|
||||||
|
- `"slot": "hooks-only"`
|
||||||
|
- `"slotNote": "Does NOT occupy the contextEngine slot. Safe to run alongside any context engine."`
|
||||||
|
- `"complements": [{ "id": "lossless-claw", ... }]`
|
||||||
|
|
||||||
|
If MC became a contextEngine:
|
||||||
|
- Users would be forced to choose between MC and lossless-claw
|
||||||
|
- Context compression is a broader, more fundamental need than working-state recovery
|
||||||
|
- Most users who want MC also want context compression -- making them mutually exclusive would reduce adoption of both
|
||||||
|
|
||||||
|
### Composite/multi-engine support
|
||||||
|
Research found **no evidence** of composite engine, engine chaining, or multi-engine support in OpenClaw:
|
||||||
|
- No `contextEngine` array support in plugin schema
|
||||||
|
- No engine composition layer in runtime code
|
||||||
|
- No roadmap references to multi-engine support in available documentation
|
||||||
|
- The design documents themselves note this as a precondition: "only pursue if... composite-engine support exists"
|
||||||
|
|
||||||
|
**That precondition has not been met.**
|
||||||
|
|
||||||
|
### Ecosystem risk
|
||||||
|
MC's hooks-only design is a competitive advantage:
|
||||||
|
- It is the only memory plugin that explicitly complements rather than competes with context engines
|
||||||
|
- Converting to a contextEngine would make MC just another exclusive-slot plugin competing for the same position
|
||||||
|
- The interop declaration in `openclaw.plugin.json` is a trust signal to users that MC respects their plugin choices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risk/Benefit Matrix
|
||||||
|
|
||||||
|
### Building a ContextEngine variant
|
||||||
|
|
||||||
|
| Factor | Assessment |
|
||||||
|
|---|---|
|
||||||
|
| **Benefit: Better prompt injection** | Low -- `prependSystemContext` already works |
|
||||||
|
| **Benefit: Full context control** | Irrelevant -- MC does not need context assembly control |
|
||||||
|
| **Benefit: Token budget awareness** | Minimal -- MC's snapshots are already small by design |
|
||||||
|
| **Risk: Breaks lossless-claw coexistence** | **Critical** -- destroys MC's key ecosystem advantage |
|
||||||
|
| **Risk: Reduced adoption** | **High** -- users forced into either/or choice |
|
||||||
|
| **Risk: Maintenance burden** | Medium -- two codepaths (hooks version + engine version) to maintain |
|
||||||
|
| **Risk: Feature creep** | High -- engine slot invites scope expansion into context compression territory |
|
||||||
|
| **Risk: No composite engine fallback** | **Critical** -- if composite support never ships, the variant is permanently exclusive |
|
||||||
|
|
||||||
|
### Keeping hooks-only
|
||||||
|
|
||||||
|
| Factor | Assessment |
|
||||||
|
|---|---|
|
||||||
|
| **Benefit: Coexists with all context engines** | **Critical** -- unique market position |
|
||||||
|
| **Benefit: Simpler architecture** | High -- one codepath, clear scope boundaries |
|
||||||
|
| **Benefit: Lower maintenance** | High -- no engine API surface to track |
|
||||||
|
| **Benefit: Aligned with design principles** | High -- scope.md principle #8: "Ecosystem compatibility matters" |
|
||||||
|
| **Risk: Missing prompt-time power** | Low -- `before_prompt_build` covers the actual need |
|
||||||
|
| **Risk: Weaker injection guarantee** | Low -- not observed as a real problem in practice |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
**Do not build a ContextEngine variant. Invest in hook-based improvements instead.**
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
1. The two preconditions from plugin-design.md Phase 5 are both unmet:
|
||||||
|
- "slot tradeoffs are acceptable" -- they are not; lossless-claw coexistence is too valuable
|
||||||
|
- "composite-engine support exists" -- it does not
|
||||||
|
2. MC's actual prompt injection needs are satisfied by `before_prompt_build` + `prependSystemContext`
|
||||||
|
3. The contextEngine API solves context assembly and compression problems that are outside MC's scope
|
||||||
|
4. Converting would destroy MC's strongest differentiator: being the only memory plugin that complements rather than competes with context engines
|
||||||
|
|
||||||
|
### What to do instead: hook improvements
|
||||||
|
|
||||||
|
#### Priority 1 -- Strengthen existing injection
|
||||||
|
- Validate `prependSystemContext` reliability across OpenClaw versions and model backends
|
||||||
|
- Add fallback to `before_agent_start` if `before_prompt_build` proves unreliable in edge cases
|
||||||
|
- Improve snapshot quality (better summarization, freshness labeling)
|
||||||
|
|
||||||
|
#### Priority 2 -- Better compaction protection
|
||||||
|
- Confirm `before_compaction` is truly synchronous (the probe exists but needs production validation)
|
||||||
|
- Add tail-message protection quality metrics
|
||||||
|
- Consider `after_compaction` hook for post-compaction state verification
|
||||||
|
|
||||||
|
#### Priority 3 -- Smarter relevance injection
|
||||||
|
- The `relevanceInjection` feature already injects historical context at startup
|
||||||
|
- Improve relevance scoring without needing engine-level token budget control
|
||||||
|
- Keep injection budget self-contained (MC manages its own token ceiling)
|
||||||
|
|
||||||
|
#### Priority 4 -- Monitor OpenClaw evolution
|
||||||
|
- Watch for composite-engine or engine-delegation support in future OpenClaw releases
|
||||||
|
- If OpenClaw adds a way for hooks to register "guaranteed system prompt sections" at engine priority without taking the slot, adopt that immediately
|
||||||
|
- Re-evaluate this decision if the exclusive-slot constraint changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conditions for revisiting this decision
|
||||||
|
|
||||||
|
Re-evaluate if ANY of these become true:
|
||||||
|
1. OpenClaw adds composite/multi-engine support (multiple contextEngines can coexist)
|
||||||
|
2. OpenClaw adds a "system prompt section" API at engine priority, available to non-engine plugins
|
||||||
|
3. `before_prompt_build` + `prependSystemContext` proves unreliable in a way that cannot be fixed via hooks
|
||||||
|
4. A significant user cohort explicitly requests engine-level context control from MC and does not use lossless-claw
|
||||||
|
|
||||||
|
Until then, the hooks-only architecture remains correct.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Document history
|
||||||
|
- 2026-03-31: Initial evaluation. Decision: do not build.
|
||||||
Reference in New Issue
Block a user