- Add workspace resolution helpers (isSubagentSession, parseParentAgentId, resolveAgentWorkspace) - Parent→Child seeding: inject parent CURRENT_STATE.md into subagent context at startup - Child→Parent recovery: new subagent_ended hook reads child unsurfaced results, merges into parent - Add subagentSeed and subagentRecovery config keys (both default: true) - Add /mc subagents command for cross-workspace state overview - Bump lifecycle plugin to v5.0.0, mc-plugin to v2.1.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
17 KiB
v3.1 Short-Term Memory Layer Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add session logging, smart tail protection during compaction, CJK token awareness, and noise filtering to the Memory Continuity plugin.
Architecture: Four independent features added to the existing lifecycle hook system. Session logs write to memory/sessions/YYYY-MM-DD.md (one file per day, append-only). Tail protection injects recent critical messages into before_compaction. CJK token estimation uses a helper function for line-count decisions. Ignore patterns filter out cron/subagent noise at agent_end.
Tech Stack: Node.js (ES modules), pure filesystem, zero dependencies
File Structure
| File | Action | Responsibility |
|---|---|---|
index.js |
Modify | Add session logging in agent_end, enhance before_compaction with tail protection, add CJK helper, add ignore-pattern filtering |
openclaw.plugin.json |
Modify | Add new config keys: sessionLogging, tailProtectCount, ignorePatterns |
mc-plugin/index.js |
Modify | Add /mc sessions subcommand, update help text |
mc-plugin/openclaw.plugin.json |
No change | — |
Task 1: CJK Token Estimation Helper
Files:
-
Modify:
index.js(after line 15, the PLACEHOLDER_VALUES block) -
Step 1: Add
estimateTokenshelper function toindex.js
Add this after the PLACEHOLDER_VALUES constant (line 15):
/**
* Estimate token count with CJK awareness.
* CJK characters ≈ 1.5 tokens each; Latin words ≈ 1 token per ~4 chars.
*/
function estimateTokens(text) {
if (!text) return 0;
// Count CJK characters (CJK Unified Ideographs + common CJK ranges)
const cjkCount = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/g) || []).length;
// Remove CJK chars, count remaining as ~1 token per 4 chars
const nonCjk = text.replace(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/g, "");
const latinTokens = Math.ceil(nonCjk.length / 4);
return Math.ceil(cjkCount * 1.5) + latinTokens;
}
- Step 2: Update
truncateinextractStateFromMessagesto use token-aware truncation
In extractStateFromMessages (around line 175), replace the existing truncate function:
// Token-aware truncation
const truncate = (s, maxTokens = 200) => {
if (estimateTokens(s) <= maxTokens) return s;
// Binary search for the right cut point
let lo = 0, hi = s.length;
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
if (estimateTokens(s.slice(0, mid)) <= maxTokens) lo = mid;
else hi = mid - 1;
}
return s.slice(0, lo) + "...";
};
- Step 3: Verify no syntax errors
Run:
cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js
Expected: no output (syntax OK)
- Step 4: Commit
git add index.js
git commit -m "feat: add CJK token-aware estimation helper (v3.1)"
Task 2: Ignore Patterns — Filter Cron/Subagent Noise
Files:
-
Modify:
index.js(agent_end hook, around line 295) -
Modify:
openclaw.plugin.json(addignorePatternsconfig) -
Step 1: Add
ignorePatternstoopenclaw.plugin.jsonconfig schema
Add after the maxArchiveCount property block (after line 29):
"ignorePatterns": {
"type": "array",
"default": [],
"description": "Regex patterns to ignore sessions (e.g. cron jobs, subagent noise). Matched against first user message."
}
And add corresponding uiHints after the maxArchiveCount hint (after line 47):
"ignorePatterns": {
"label": "Ignore patterns",
"help": "Skip state extraction for sessions matching these patterns (regex)"
}
- Step 2: Add ignore-pattern check in
agent_endhook inindex.js
In the agent_end hook, after the realUserMsgs filtering block (after line 321), add:
// Check ignore patterns — skip sessions matching cron/subagent noise
const ignorePatterns = (config.ignorePatterns || [])
.map(p => { try { return new RegExp(p, "i"); } catch { return null; } })
.filter(Boolean);
if (ignorePatterns.length > 0 && realUserMsgs.length > 0) {
const firstMsg = realUserMsgs[0];
if (ignorePatterns.some(re => re.test(firstMsg))) {
log.info?.("[memory-continuity] Session matches ignorePattern, skipping");
return;
}
}
- Step 3: Verify no syntax errors
Run:
cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js
Expected: no output (syntax OK)
- Step 4: Commit
git add index.js openclaw.plugin.json
git commit -m "feat: add ignorePatterns to filter cron/subagent noise (v3.1)"
Task 3: Session Logging
Files:
-
Modify:
index.js(addwriteSessionLoghelper + call it inagent_end) -
Step 1: Add
writeSessionLoghelper function toindex.js
Add after the cleanupMemoryDir function (after line 140):
/**
* Append a session summary to the daily session log.
* File: memory/sessions/YYYY-MM-DD.md (one per day, append-only)
*/
function writeSessionLog(workspaceDir, messages, config = {}) {
if (config.sessionLogging === false) return;
if (!messages || messages.length === 0) return;
const now = new Date();
const pad = (n) => String(n).padStart(2, "0");
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
const sessionsDir = path.join(workspaceDir, "memory", "sessions");
fs.mkdirSync(sessionsDir, { recursive: true });
const logFile = path.join(sessionsDir, `${dateStr}.md`);
// Extract first meaningful user message as topic
let topic = "(no topic)";
for (const msg of messages) {
if (msg?.role !== "user") continue;
const text = typeof msg.content === "string"
? msg.content
: Array.isArray(msg.content)
? msg.content.filter(b => b?.type === "text").map(b => b.text).join("\n")
: "";
const cleaned = text
.replace(/^Conversation info \(untrusted metadata\):[\s\S]*?\n\n/m, "")
.replace(/^Sender \(untrusted metadata\):[\s\S]*?\n\n/m, "")
.trim();
if (cleaned.length > 10) {
topic = cleaned.split("\n")[0].slice(0, 120);
break;
}
}
// Count messages by role
const userCount = messages.filter(m => m?.role === "user").length;
const assistantCount = messages.filter(m => m?.role === "assistant").length;
const totalTokens = estimateTokens(
messages.map(m => typeof m?.content === "string" ? m.content : "").join("")
);
// Build log entry
const entry = [
`### ${timeStr}`,
`- **Topic:** ${topic}`,
`- **Messages:** ${userCount} user / ${assistantCount} assistant`,
`- **Est. tokens:** ~${totalTokens}`,
"",
].join("\n");
// Append to daily log (create with header if new)
if (!fs.existsSync(logFile)) {
const header = `# Session Log — ${dateStr}\n\n`;
fs.writeFileSync(logFile, header + entry, "utf8");
} else {
fs.appendFileSync(logFile, entry, "utf8");
}
}
- Step 2: Add
sessionLoggingconfig toopenclaw.plugin.json
Add after the ignorePatterns property in the config schema:
"sessionLogging": {
"type": "boolean",
"default": true,
"description": "Write session summaries to memory/sessions/ daily logs"
}
And add corresponding uiHints:
"sessionLogging": {
"label": "Session logging",
"help": "Append session summaries to daily markdown logs"
}
- Step 3: Call
writeSessionLogfromagent_endhook
In the agent_end hook, right before the const existing = readFile(statePath); line (around line 324), add:
// Write session log entry
writeSessionLog(ws, messages, config);
- Step 4: Verify no syntax errors
Run:
cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js
Expected: no output (syntax OK)
- Step 5: Commit
git add index.js openclaw.plugin.json
git commit -m "feat: add session logging to memory/sessions/ daily files (v3.1)"
Task 4: Smart Tail Protection in before_compaction
Files:
-
Modify:
index.js(enhancebefore_compactionhook, around line 254) -
Modify:
openclaw.plugin.json(addtailProtectCountconfig) -
Step 1: Add
tailProtectCountconfig toopenclaw.plugin.json
Add after the sessionLogging property in the config schema:
"tailProtectCount": {
"type": "number",
"default": 3,
"description": "Number of recent critical message pairs to protect during compaction"
}
And add corresponding uiHints:
"tailProtectCount": {
"label": "Tail protect count",
"help": "Keep N recent user/assistant pairs visible after compaction"
}
- Step 2: Add
extractTailMessageshelper toindex.js
Add after the writeSessionLog function:
/**
* Extract the last N meaningful user/assistant exchange pairs from messages.
* Returns a formatted string for injection into compaction context.
*/
function extractTailMessages(messages, count = 3) {
if (!messages || messages.length === 0) return null;
// Walk backwards, collect up to `count` user+assistant pairs
const pairs = [];
let i = messages.length - 1;
while (i >= 0 && pairs.length < count) {
// Find assistant message
while (i >= 0 && messages[i]?.role !== "assistant") i--;
if (i < 0) break;
const assistantMsg = messages[i];
i--;
// Find preceding user message
while (i >= 0 && messages[i]?.role !== "user") i--;
if (i < 0) break;
const userMsg = messages[i];
i--;
const getText = (msg) => {
if (typeof msg?.content === "string") return msg.content;
if (Array.isArray(msg?.content)) {
return msg.content.filter(b => b?.type === "text").map(b => b.text).join("\n");
}
return "";
};
const userText = getText(userMsg).trim();
const assistantText = getText(assistantMsg).trim();
// Skip trivial exchanges
if (userText.length < 10 && assistantText.length < 20) continue;
// Token-aware truncation per message
const maxPerMsg = 150;
const truncMsg = (s) => {
if (estimateTokens(s) <= maxPerMsg) return s;
let lo = 0, hi = s.length;
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
if (estimateTokens(s.slice(0, mid)) <= maxPerMsg) lo = mid;
else hi = mid - 1;
}
return s.slice(0, lo) + "...";
};
pairs.unshift({ user: truncMsg(userText), assistant: truncMsg(assistantText) });
}
if (pairs.length === 0) return null;
const lines = ["=== RECENT EXCHANGE (protected) ==="];
for (const p of pairs) {
lines.push(`User: ${p.user}`);
lines.push(`Assistant: ${p.assistant}`);
lines.push("---");
}
lines.push("=== END RECENT EXCHANGE ===");
return lines.join("\n");
}
- Step 3: Enhance
before_compactionhook to inject tail messages
Replace the existing before_compaction hook body (lines 254-270) with:
api.on("before_compaction", async (_event, _ctx) => {
const ws = _ctx?.workspaceDir;
const config = getConfig();
const statePath = resolveStatePath(ws);
if (!statePath) return;
const md = readFile(statePath);
if (!md) return;
const snapshot = buildSnapshot(md);
if (!snapshot) return;
log.info?.("[memory-continuity] Injecting state before compaction");
// Smart tail protection: also inject recent critical messages
const tailCount = config.tailProtectCount ?? 3;
const messages = _event?.messages;
const tail = tailCount > 0 ? extractTailMessages(messages, tailCount) : null;
const parts = [snapshot];
if (tail) {
parts.push(tail);
log.info?.("[memory-continuity] Tail protection: injected recent exchanges");
}
return {
prependSystemContext: parts.join("\n\n"),
};
}, { priority: 10 });
- Step 4: Verify no syntax errors
Run:
cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js
Expected: no output (syntax OK)
- Step 5: Commit
git add index.js openclaw.plugin.json
git commit -m "feat: smart tail protection during compaction (v3.1)"
Task 5: /mc sessions Command
Files:
-
Modify:
mc-plugin/index.js(addcmdSessionsfunction + wire into switch) -
Step 1: Add
cmdSessionshandler tomc-plugin/index.js
Add after the cmdExport function (before cmdHelp, around line 437):
function cmdSessions(args) {
const parts = (args || "").trim().split(/\s+/);
const agent = parts.find(p => !p.startsWith("-") && !p.startsWith("2")) || "main";
const dateArg = parts.find(p => /^\d{4}-\d{2}-\d{2}$/.test(p));
const memDir = resolveMemDir(agent);
if (!memDir) return `Agent "${agent}" not found.`;
const sessionsDir = path.join(memDir, "sessions");
let files;
try { files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".md")).sort().reverse(); }
catch { return `No session logs for "${agent}".`; }
if (!files.length) return `No session logs for "${agent}".`;
// If date specified, show that day's log
if (dateArg) {
const target = `${dateArg}.md`;
const content = readFile(path.join(sessionsDir, target));
if (!content) return `No session log for ${dateArg}.`;
// Truncate to last 50 lines to stay compact
const lines = content.split("\n");
const shown = lines.length > 50 ? lines.slice(-50) : lines;
let out = shown.join("\n");
if (lines.length > 50) out = `... (${lines.length - 50} earlier lines omitted)\n\n` + out;
return out;
}
// List recent session logs
let out = `Session Logs: ${agent} (${files.length} days)\n`;
out += "─────────────────────────────\n";
for (const f of files.slice(0, 14)) {
const date = f.replace(".md", "");
const content = readFile(path.join(sessionsDir, f));
// Count session entries (### HH:MM headers)
const sessionCount = content ? (content.match(/^### \d{2}:\d{2}/gm) || []).length : 0;
out += `${date} ${String(sessionCount).padStart(3)} session(s)\n`;
}
if (files.length > 14) out += `\n ... and ${files.length - 14} more days`;
out += `\n\nUse /mc sessions <YYYY-MM-DD> to view a specific day.`;
return out.trimEnd();
}
- Step 2: Wire
sessionsinto the command switch
In the switch statement (around line 471), add a new case before help:
case "sessions": text = cmdSessions(subargs || null); break;
- Step 3: Update
cmdHelpto includesessions
Add this line in the help text after the /mc state --all line:
/mc sessions [date] Session logs (daily activity)
- Step 4: Verify no syntax errors
Run:
cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c mc-plugin/index.js
Expected: no output (syntax OK)
- Step 5: Commit
git add mc-plugin/index.js
git commit -m "feat: add /mc sessions command for daily session logs (v3.1)"
Task 6: Version Bump and Final Verification
Files:
-
Modify:
openclaw.plugin.json(version 3.0.0 → 3.1.0) -
Modify:
mc-plugin/openclaw.plugin.json(version if present) -
Modify:
package.json(version if present) -
Step 1: Bump version in
openclaw.plugin.jsonto 3.1.0
Change "version": "3.0.0" to "version": "3.1.0".
- Step 2: Bump version in
package.jsonto 3.1.0
Change "version" value to "3.1.0".
- Step 3: Verify both plugins parse correctly
Run:
cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js && node -c mc-plugin/index.js && node -e "JSON.parse(require('fs').readFileSync('openclaw.plugin.json','utf8'))" && echo "All OK"
Expected: All OK
- Step 4: Commit and tag
git add openclaw.plugin.json package.json
git commit -m "release: v3.1.0 — short-term memory layer"
- Step 5: Push to GitHub
git push
Summary of New Config Keys
| Key | Type | Default | Purpose |
|---|---|---|---|
ignorePatterns |
string[] |
[] |
Regex patterns to skip sessions (cron/subagent) |
sessionLogging |
boolean |
true |
Enable daily session log files |
tailProtectCount |
number |
3 |
Recent exchange pairs to protect during compaction |
Summary of New/Changed Hooks
| Hook | Change |
|---|---|
agent_end |
+session logging, +ignore pattern filtering, +CJK-aware truncation |
before_compaction |
+tail message protection |
New Command
| Command | Purpose |
|---|---|
/mc sessions [date] |
List daily session logs or view specific day |