Files
memory-continuity/docs/superpowers/plans/2026-03-31-v3.2-mid-term-memory.md
T
taodengandClaude Opus 4.6 69df869335 release: v5.0.0 — conservative subagent support (parent seed + child recovery)
- 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>
2026-03-31 17:35:29 +10:00

21 KiB

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:

      "summaryEnabled": {
        "type": "boolean",
        "default": true,
        "description": "Generate daily and weekly summaries from session logs"
      }

Add corresponding uiHints after the tailProtectCount hint:

    "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:

/**
 * 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:

      // Generate daily summary for previous day if needed
      generateDailySummary(ws, 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: 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:

/**
 * 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:

      generateWeeklySummary(ws, config);
  • Step 3: Verify no syntax errors

Run:

cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js
  • Step 4: Commit
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:

/**
 * 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:

  // Extract and index #tags from the topic
  updateTagIndex(workspaceDir, topic, dateStr);
  • Step 3: Verify no syntax errors

Run:

cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js
  • Step 4: Commit
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:

function cmdSearch(args) {
  if (!args) return "Usage: /mc search <keyword> [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:

cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c mc-plugin/index.js
  • Step 3: Commit
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:

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:

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 <YYYY-MM-DD> or /mc summary <YYYY-Www> 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:

          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:

cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c mc-plugin/index.js
  • Step 6: Commit
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:

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
git add openclaw.plugin.json package.json
git commit -m "release: v3.2.0 — mid-term memory layer"
  • Step 4: Push to GitHub
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 <date> View specific daily/weekly summary
/mc tags [agent] View tag index