# v3.2 Mid-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 layered markdown summaries (daily/weekly), enhanced cross-source search, and automatic tag extraction to the Memory Continuity plugin. **Architecture:** Three independent features layered on v3.1's session logging. Daily summaries are generated from session logs at `agent_end` when the day changes. Weekly summaries roll up dailies every Monday. Enhanced search extends the existing `/mc search` to cover sessions + summaries with context lines. Auto-tagging scans session entries for `#tag` patterns and maintains a `memory/tags.md` index. All pure markdown, zero dependencies. **Tech Stack:** Node.js (ES modules), pure filesystem, zero dependencies --- ## File Structure | File | Action | Responsibility | |------|--------|---------------| | `index.js` | Modify | Add `generateDailySummary`, `generateWeeklySummary`, call from `agent_end` | | `openclaw.plugin.json` | Modify | Add `summaryEnabled` config | | `mc-plugin/index.js` | Modify | Enhance `cmdSearch` to cover sessions+summaries, add `/mc tags` and `/mc summary` commands | --- ### Task 1: Daily Summary Generation **Files:** - Modify: `index.js` (add helper + call in agent_end) - Modify: `openclaw.plugin.json` (add `summaryEnabled` config) - [ ] **Step 1: Add `summaryEnabled` config to `openclaw.plugin.json`** Read `openclaw.plugin.json`. Add after the `tailProtectCount` property in configSchema: ```json "summaryEnabled": { "type": "boolean", "default": true, "description": "Generate daily and weekly summaries from session logs" } ``` Add corresponding uiHints after the `tailProtectCount` hint: ```json "summaryEnabled": { "label": "Summary generation", "help": "Auto-generate daily/weekly summaries from session logs" } ``` - [ ] **Step 2: Add `generateDailySummary` helper to `index.js`** Read `index.js`. Add after the `extractTailMessages` function (ends around line 302), before `extractStateFromMessages`: ```javascript /** * Generate a daily summary from the previous day's session log. * Only runs when today differs from the last summary date. * Output: memory/summaries/daily/YYYY-MM-DD.md */ function generateDailySummary(workspaceDir, config = {}) { if (config.summaryEnabled === false) return; const pad = (n) => String(n).padStart(2, "0"); const now = new Date(); const today = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; // Check yesterday's date const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); const yStr = `${yesterday.getFullYear()}-${pad(yesterday.getMonth() + 1)}-${pad(yesterday.getDate())}`; const summaryDir = path.join(workspaceDir, "memory", "summaries", "daily"); const summaryFile = path.join(summaryDir, `${yStr}.md`); // Skip if summary already exists for yesterday if (fs.existsSync(summaryFile)) return; // Read yesterday's session log const sessionFile = path.join(workspaceDir, "memory", "sessions", `${yStr}.md`); const sessionContent = readFile(sessionFile); if (!sessionContent) return; // No sessions yesterday // Parse session entries const entries = sessionContent.split(/^### /gm).filter(e => e.trim()); if (entries.length === 0) return; const topics = []; let totalUser = 0; let totalAssistant = 0; let totalTokens = 0; for (const entry of entries) { const topicMatch = entry.match(/\*\*Topic:\*\*\s*(.+)/); const msgMatch = entry.match(/\*\*Messages:\*\*\s*(\d+)\s*user\s*\/\s*(\d+)\s*assistant/); const tokenMatch = entry.match(/\*\*Est\. tokens:\*\*\s*~(\d+)/); if (topicMatch) topics.push(topicMatch[1].trim()); if (msgMatch) { totalUser += parseInt(msgMatch[1]); totalAssistant += parseInt(msgMatch[2]); } if (tokenMatch) totalTokens += parseInt(tokenMatch[1]); } // Build daily summary const summary = [ `# Daily Summary — ${yStr}`, "", `**Sessions:** ${entries.length}`, `**Messages:** ${totalUser} user / ${totalAssistant} assistant`, `**Est. tokens:** ~${totalTokens}`, "", "## Topics", ...topics.map((t, i) => `${i + 1}. ${t}`), "", ].join("\n"); fs.mkdirSync(summaryDir, { recursive: true }); fs.writeFileSync(summaryFile, summary, "utf8"); } ``` - [ ] **Step 3: Call `generateDailySummary` from `agent_end` hook** In the `agent_end` hook, right after the `writeSessionLog(ws, messages, config);` line (around line 515), add: ```javascript // Generate daily summary for previous day if needed generateDailySummary(ws, config); ``` - [ ] **Step 4: Verify no syntax errors** Run: ```bash cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js ``` Expected: no output (syntax OK) - [ ] **Step 5: Commit** ```bash git add index.js openclaw.plugin.json git commit -m "feat: auto-generate daily summaries from session logs (v3.2)" ``` --- ### Task 2: Weekly Summary Generation **Files:** - Modify: `index.js` (add `generateWeeklySummary` + call in agent_end) - [ ] **Step 1: Add `generateWeeklySummary` helper to `index.js`** Read `index.js`. Add after the `generateDailySummary` function: ```javascript /** * Generate a weekly summary by rolling up daily summaries. * Runs on Monday, summarizing the previous week (Mon-Sun). * Output: memory/summaries/weekly/YYYY-Www.md (ISO week number) */ function generateWeeklySummary(workspaceDir, config = {}) { if (config.summaryEnabled === false) return; const now = new Date(); // Only run on Mondays if (now.getDay() !== 1) return; const pad = (n) => String(n).padStart(2, "0"); // Calculate previous week's Monday const prevMonday = new Date(now); prevMonday.setDate(prevMonday.getDate() - 7); // ISO week number const jan1 = new Date(prevMonday.getFullYear(), 0, 1); const weekNum = Math.ceil(((prevMonday - jan1) / 86400000 + jan1.getDay() + 1) / 7); const weekLabel = `${prevMonday.getFullYear()}-W${pad(weekNum)}`; const weeklyDir = path.join(workspaceDir, "memory", "summaries", "weekly"); const weeklyFile = path.join(weeklyDir, `${weekLabel}.md`); // Skip if already generated if (fs.existsSync(weeklyFile)) return; // Collect daily summaries for the 7 days of previous week const dailyDir = path.join(workspaceDir, "memory", "summaries", "daily"); const dailies = []; let weekSessions = 0; let weekUser = 0; let weekAssistant = 0; let weekTokens = 0; const allTopics = []; for (let d = 0; d < 7; d++) { const day = new Date(prevMonday); day.setDate(day.getDate() + d); const dayStr = `${day.getFullYear()}-${pad(day.getMonth() + 1)}-${pad(day.getDate())}`; const dailyFile = path.join(dailyDir, `${dayStr}.md`); const content = readFile(dailyFile); if (!content) continue; dailies.push(dayStr); const sessMatch = content.match(/\*\*Sessions:\*\*\s*(\d+)/); const msgMatch = content.match(/\*\*Messages:\*\*\s*(\d+)\s*user\s*\/\s*(\d+)\s*assistant/); const tokenMatch = content.match(/\*\*Est\. tokens:\*\*\s*~(\d+)/); if (sessMatch) weekSessions += parseInt(sessMatch[1]); if (msgMatch) { weekUser += parseInt(msgMatch[1]); weekAssistant += parseInt(msgMatch[2]); } if (tokenMatch) weekTokens += parseInt(tokenMatch[1]); // Extract topics const topicSection = content.split("## Topics")[1]; if (topicSection) { const topics = topicSection.match(/^\d+\.\s+(.+)$/gm); if (topics) allTopics.push(...topics.map(t => t.replace(/^\d+\.\s+/, "").trim())); } } if (dailies.length === 0) return; // Deduplicate topics (keep first occurrence) const seen = new Set(); const uniqueTopics = allTopics.filter(t => { const key = t.toLowerCase().slice(0, 50); if (seen.has(key)) return false; seen.add(key); return true; }); const summary = [ `# Weekly Summary — ${weekLabel}`, `> ${dailies[0]} to ${dailies[dailies.length - 1]}`, "", `**Active days:** ${dailies.length}/7`, `**Total sessions:** ${weekSessions}`, `**Total messages:** ${weekUser} user / ${weekAssistant} assistant`, `**Est. total tokens:** ~${weekTokens}`, "", "## Key Topics", ...uniqueTopics.slice(0, 20).map((t, i) => `${i + 1}. ${t}`), "", ].join("\n"); fs.mkdirSync(weeklyDir, { recursive: true }); fs.writeFileSync(weeklyFile, summary, "utf8"); } ``` - [ ] **Step 2: Call `generateWeeklySummary` from `agent_end` hook** Right after the `generateDailySummary(ws, config);` line, add: ```javascript generateWeeklySummary(ws, config); ``` - [ ] **Step 3: Verify no syntax errors** Run: ```bash cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js ``` - [ ] **Step 4: Commit** ```bash git add index.js git commit -m "feat: auto-generate weekly summaries from daily rollups (v3.2)" ``` --- ### Task 3: Auto-Tagging — Extract `#tag` from Session Logs **Files:** - Modify: `index.js` (add `updateTagIndex` helper + call in `writeSessionLog`) - [ ] **Step 1: Add `updateTagIndex` helper to `index.js`** Read `index.js`. Add after the `generateWeeklySummary` function: ```javascript /** * Extract #tags from a session topic and update the tag index. * Tags file: memory/tags.md — simple markdown index mapping tags to dates. */ function updateTagIndex(workspaceDir, topic, dateStr) { if (!topic) return; // Extract #tags (word chars + hyphens after #) const tags = topic.match(/#[\w-]+/g); if (!tags || tags.length === 0) return; const tagsFile = path.join(workspaceDir, "memory", "tags.md"); let existing = readFile(tagsFile) || "# Tag Index\n\n"; for (const tag of tags) { const normalizedTag = tag.toLowerCase(); // Check if this tag+date combo already exists if (existing.includes(`${normalizedTag}`) && existing.includes(dateStr)) continue; // Find or create tag section const tagHeader = `## ${normalizedTag}`; if (existing.includes(tagHeader)) { // Append date to existing tag section existing = existing.replace( tagHeader, `${tagHeader}\n- ${dateStr}` ); } else { // Add new tag section existing += `${tagHeader}\n- ${dateStr}\n\n`; } } writeFile(tagsFile, existing); } ``` - [ ] **Step 2: Call `updateTagIndex` from `writeSessionLog`** In the `writeSessionLog` function, right after the `fs.appendFileSync(logFile, prefix + entry, "utf8");` line (the last line of the function), add: ```javascript // Extract and index #tags from the topic updateTagIndex(workspaceDir, topic, dateStr); ``` - [ ] **Step 3: Verify no syntax errors** Run: ```bash cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js ``` - [ ] **Step 4: Commit** ```bash git add index.js git commit -m "feat: auto-extract #tags from sessions to memory/tags.md (v3.2)" ``` --- ### Task 4: Enhanced Search — Cross Sessions + Summaries **Files:** - Modify: `mc-plugin/index.js` (enhance `cmdSearch`) - [ ] **Step 1: Enhance `cmdSearch` to search sessions and summaries** Read `mc-plugin/index.js`. Replace the entire `cmdSearch` function (lines 220-279) with: ```javascript function cmdSearch(args) { if (!args) return "Usage: /mc search [agent]\nSearches state, archives, sessions, and summaries."; const parts = args.trim().split(/\s+/); let keyword, agent; const agents = discoverAgents(); const agentNames = new Set(agents.map(a => a.name)); if (parts.length > 1 && agentNames.has(parts[parts.length - 1])) { agent = parts.pop(); keyword = parts.join(" "); } else { keyword = parts.join(" "); agent = null; } const re = new RegExp(keyword, "gi"); const results = []; const searchAgents = agent ? [{ name: agent, memDir: resolveMemDir(agent) }] : agents; for (const { name, memDir } of searchAgents) { if (!memDir) continue; // 1. Search current state const state = readFile(path.join(memDir, "CURRENT_STATE.md")); if (state && re.test(state)) { re.lastIndex = 0; const lines = state.split("\n").filter(l => re.test(l)); results.push({ agent: name, source: "CURRENT_STATE", type: "state", matches: lines.slice(0, 3) }); } // 2. Search archives (most recent 30) const archiveDir = path.join(memDir, "session_archive"); try { const files = fs.readdirSync(archiveDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 30)) { re.lastIndex = 0; const content = readFile(path.join(archiveDir, f)); if (content && re.test(content)) { re.lastIndex = 0; const lines = content.split("\n").filter(l => re.test(l)); results.push({ agent: name, source: f.replace(".md", ""), type: "archive", matches: lines.slice(0, 2) }); } } } catch {} // 3. Search session logs (most recent 14 days) const sessionsDir = path.join(memDir, "sessions"); try { const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 14)) { re.lastIndex = 0; const content = readFile(path.join(sessionsDir, f)); if (content && re.test(content)) { re.lastIndex = 0; const lines = content.split("\n").filter(l => re.test(l)); results.push({ agent: name, source: f.replace(".md", ""), type: "session", matches: lines.slice(0, 2) }); } } } catch {} // 4. Search summaries (daily + weekly) for (const sub of ["daily", "weekly"]) { const sumDir = path.join(memDir, "summaries", sub); try { const files = fs.readdirSync(sumDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 10)) { re.lastIndex = 0; const content = readFile(path.join(sumDir, f)); if (content && re.test(content)) { re.lastIndex = 0; const lines = content.split("\n").filter(l => re.test(l)); results.push({ agent: name, source: f.replace(".md", ""), type: sub, matches: lines.slice(0, 2) }); } } } catch {} } } if (!results.length) return `No matches for "${keyword}".`; // Group by type for clearer output let out = `Search: "${keyword}" (${results.length} hits)\n`; out += "─────────────────────────────\n"; const typeOrder = ["state", "archive", "session", "daily", "weekly"]; const typeLabels = { state: "State", archive: "Archive", session: "Session", daily: "Daily", weekly: "Weekly" }; for (const type of typeOrder) { const group = results.filter(r => r.type === type); if (group.length === 0) continue; out += `\n[${typeLabels[type]}]\n`; for (const r of group.slice(0, 5)) { out += ` ${r.agent}/${r.source}\n`; for (const line of r.matches) { out += ` ${truncate(line.trim(), 75)}\n`; } } if (group.length > 5) out += ` ... +${group.length - 5} more\n`; } return out.trimEnd(); } ``` - [ ] **Step 2: Verify no syntax errors** Run: ```bash cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c mc-plugin/index.js ``` - [ ] **Step 3: Commit** ```bash git add mc-plugin/index.js git commit -m "feat: enhanced search across sessions and summaries (v3.2)" ``` --- ### Task 5: `/mc tags` and `/mc summary` Commands **Files:** - Modify: `mc-plugin/index.js` (add two command handlers + wire switch + update help) - [ ] **Step 1: Add `cmdTags` handler to `mc-plugin/index.js`** Read `mc-plugin/index.js`. Add before the `cmdHelp` function: ```javascript function cmdTags(args) { const agent = args || "main"; const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; const tagsFile = path.join(memDir, "tags.md"); const content = readFile(tagsFile); if (!content) return `No tags for "${agent}".`; // Parse tags and their date counts const tagSections = content.split(/^## /gm).filter(s => s.trim()); if (tagSections.length === 0) return `No tags for "${agent}".`; let out = `Tags: ${agent} (${tagSections.length} tags)\n`; out += "─────────────────────────────\n"; const tags = []; for (const section of tagSections) { const lines = section.trim().split("\n"); const tag = lines[0].trim(); if (!tag.startsWith("#")) continue; const dates = lines.filter(l => l.startsWith("- ")); tags.push({ tag, count: dates.length, latest: dates[0]?.replace("- ", "") || "?" }); } // Sort by count descending tags.sort((a, b) => b.count - a.count); for (const { tag, count, latest } of tags.slice(0, 30)) { out += `${tag.padEnd(25)} ${String(count).padStart(3)} days latest: ${latest}\n`; } if (tags.length > 30) out += `\n ... and ${tags.length - 30} more tags`; return out.trimEnd(); } ``` - [ ] **Step 2: Add `cmdSummary` handler** Add after `cmdTags`: ```javascript function cmdSummary(args) { const parts = (args || "").trim().split(/\s+/); const agent = parts.find(p => !p.startsWith("-") && !/^\d{4}/.test(p) && p !== "daily" && p !== "weekly") || "main"; const typeArg = parts.find(p => p === "daily" || p === "weekly"); const dateArg = parts.find(p => /^\d{4}/.test(p)); const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; // If specific date given, show that summary if (dateArg) { const type = typeArg || (dateArg.includes("W") ? "weekly" : "daily"); const sumFile = path.join(memDir, "summaries", type, `${dateArg}.md`); const content = readFile(sumFile); if (!content) return `No ${type} summary for ${dateArg}.`; return content; } // List available summaries const types = typeArg ? [typeArg] : ["daily", "weekly"]; let out = `Summaries: ${agent}\n`; out += "─────────────────────────────\n"; for (const type of types) { const sumDir = path.join(memDir, "summaries", type); let files; try { files = fs.readdirSync(sumDir).filter(f => f.endsWith(".md")).sort().reverse(); } catch { continue; } if (files.length === 0) continue; out += `\n[${type.charAt(0).toUpperCase() + type.slice(1)}] (${files.length})\n`; for (const f of files.slice(0, 10)) { const name = f.replace(".md", ""); const content = readFile(path.join(sumDir, f)); const sessMatch = content?.match(/\*\*(?:Total s|S)essions:\*\*\s*(\d+)/); const sessions = sessMatch ? sessMatch[1] : "?"; out += ` ${name} ${sessions} session(s)\n`; } if (files.length > 10) out += ` ... +${files.length - 10} more\n`; } out += `\nUse /mc summary or /mc summary to view details.`; return out.trimEnd(); } ``` - [ ] **Step 3: Wire `tags` and `summary` into the command switch** In the switch statement, add before the `help` case: ```javascript case "tags": text = cmdTags(subargs || null); break; case "summary": text = cmdSummary(subargs || null); break; ``` - [ ] **Step 4: Update `cmdHelp` to include new commands** Add these lines after `/mc sessions [date]`: ``` /mc summary [daily|weekly] List or view summaries /mc tags [agent] View tag index ``` - [ ] **Step 5: Verify no syntax errors** Run: ```bash cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c mc-plugin/index.js ``` - [ ] **Step 6: Commit** ```bash git add mc-plugin/index.js git commit -m "feat: add /mc tags and /mc summary commands (v3.2)" ``` --- ### Task 6: Version Bump and Final Verification **Files:** - Modify: `openclaw.plugin.json` (version 3.1.0 → 3.2.0) - Modify: `package.json` (version 3.1.0 → 3.2.0) - [ ] **Step 1: Bump version in both files to 3.2.0** Change `"version": "3.1.0"` to `"version": "3.2.0"` in both `openclaw.plugin.json` and `package.json`. - [ ] **Step 2: Verify all files parse correctly** Run: ```bash 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 3: Commit** ```bash git add openclaw.plugin.json package.json git commit -m "release: v3.2.0 — mid-term memory layer" ``` - [ ] **Step 4: Push to GitHub** ```bash git push ``` --- ## Summary of New Features | Feature | Files | Description | |---------|-------|-------------| | Daily summaries | `memory/summaries/daily/YYYY-MM-DD.md` | Auto-generated from previous day's session log | | Weekly summaries | `memory/summaries/weekly/YYYY-Www.md` | Rolls up daily summaries every Monday | | Auto-tagging | `memory/tags.md` | Extracts `#tag` from session topics, maintains index | | Enhanced search | `/mc search` | Now covers state + archives + sessions + summaries, grouped by type | | `/mc summary` | mc-plugin | List/view daily and weekly summaries | | `/mc tags` | mc-plugin | View tag index with counts and latest dates | ## New Config Keys | Key | Type | Default | Purpose | |-----|------|---------|---------| | `summaryEnabled` | `boolean` | `true` | Enable/disable summary generation | ## New Commands | Command | Purpose | |---------|---------| | `/mc summary [daily\|weekly]` | List summaries or view specific one | | `/mc summary ` | View specific daily/weekly summary | | `/mc tags [agent]` | View tag index |