From 69df86933524bcb8f0bb0f0bf48ba7e1dbac5c86 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Tue, 31 Mar 2026 17:35:29 +1000 Subject: [PATCH] =?UTF-8?q?release:=20v5.0.0=20=E2=80=94=20conservative=20?= =?UTF-8?q?subagent=20support=20(parent=20seed=20+=20child=20recovery)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../2026-03-31-v3.1-short-term-memory.md | 566 +++++++++++++++ .../plans/2026-03-31-v3.2-mid-term-memory.md | 674 ++++++++++++++++++ .../plans/2026-03-31-v5.0-subagent-support.md | 556 +++++++++++++++ index.js | 205 ++++++ mc-plugin/index.js | 38 +- mc-plugin/openclaw.plugin.json | 2 +- mc-plugin/package.json | 2 +- openclaw.plugin.json | 20 +- package.json | 2 +- 9 files changed, 2060 insertions(+), 5 deletions(-) create mode 100644 docs/superpowers/plans/2026-03-31-v3.1-short-term-memory.md create mode 100644 docs/superpowers/plans/2026-03-31-v3.2-mid-term-memory.md create mode 100644 docs/superpowers/plans/2026-03-31-v5.0-subagent-support.md diff --git a/docs/superpowers/plans/2026-03-31-v3.1-short-term-memory.md b/docs/superpowers/plans/2026-03-31-v3.1-short-term-memory.md new file mode 100644 index 0000000..7a827cd --- /dev/null +++ b/docs/superpowers/plans/2026-03-31-v3.1-short-term-memory.md @@ -0,0 +1,566 @@ +# 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 `estimateTokens` helper function to `index.js`** + +Add this after the `PLACEHOLDER_VALUES` constant (line 15): + +```javascript +/** + * 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 `truncate` in `extractStateFromMessages` to use token-aware truncation** + +In `extractStateFromMessages` (around line 175), replace the existing `truncate` function: + +```javascript + // 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: +```bash +cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js +``` +Expected: no output (syntax OK) + +- [ ] **Step 4: Commit** + +```bash +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` (add `ignorePatterns` config) + +- [ ] **Step 1: Add `ignorePatterns` to `openclaw.plugin.json` config schema** + +Add after the `maxArchiveCount` property block (after line 29): + +```json + "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): + +```json + "ignorePatterns": { + "label": "Ignore patterns", + "help": "Skip state extraction for sessions matching these patterns (regex)" + } +``` + +- [ ] **Step 2: Add ignore-pattern check in `agent_end` hook in `index.js`** + +In the `agent_end` hook, after the `realUserMsgs` filtering block (after line 321), add: + +```javascript + // 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: +```bash +cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c index.js +``` +Expected: no output (syntax OK) + +- [ ] **Step 4: Commit** + +```bash +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` (add `writeSessionLog` helper + call it in `agent_end`) + +- [ ] **Step 1: Add `writeSessionLog` helper function to `index.js`** + +Add after the `cleanupMemoryDir` function (after line 140): + +```javascript +/** + * 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 `sessionLogging` config to `openclaw.plugin.json`** + +Add after the `ignorePatterns` property in the config schema: + +```json + "sessionLogging": { + "type": "boolean", + "default": true, + "description": "Write session summaries to memory/sessions/ daily logs" + } +``` + +And add corresponding uiHints: + +```json + "sessionLogging": { + "label": "Session logging", + "help": "Append session summaries to daily markdown logs" + } +``` + +- [ ] **Step 3: Call `writeSessionLog` from `agent_end` hook** + +In the `agent_end` hook, right before the `const existing = readFile(statePath);` line (around line 324), add: + +```javascript + // Write session log entry + writeSessionLog(ws, messages, 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: add session logging to memory/sessions/ daily files (v3.1)" +``` + +--- + +### Task 4: Smart Tail Protection in `before_compaction` + +**Files:** +- Modify: `index.js` (enhance `before_compaction` hook, around line 254) +- Modify: `openclaw.plugin.json` (add `tailProtectCount` config) + +- [ ] **Step 1: Add `tailProtectCount` config to `openclaw.plugin.json`** + +Add after the `sessionLogging` property in the config schema: + +```json + "tailProtectCount": { + "type": "number", + "default": 3, + "description": "Number of recent critical message pairs to protect during compaction" + } +``` + +And add corresponding uiHints: + +```json + "tailProtectCount": { + "label": "Tail protect count", + "help": "Keep N recent user/assistant pairs visible after compaction" + } +``` + +- [ ] **Step 2: Add `extractTailMessages` helper to `index.js`** + +Add after the `writeSessionLog` function: + +```javascript +/** + * 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_compaction` hook to inject tail messages** + +Replace the existing `before_compaction` hook body (lines 254-270) with: + +```javascript + 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: +```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: smart tail protection during compaction (v3.1)" +``` + +--- + +### Task 5: `/mc sessions` Command + +**Files:** +- Modify: `mc-plugin/index.js` (add `cmdSessions` function + wire into switch) + +- [ ] **Step 1: Add `cmdSessions` handler to `mc-plugin/index.js`** + +Add after the `cmdExport` function (before `cmdHelp`, around line 437): + +```javascript +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 to view a specific day.`; + return out.trimEnd(); +} +``` + +- [ ] **Step 2: Wire `sessions` into the command switch** + +In the switch statement (around line 471), add a new case before `help`: + +```javascript + case "sessions": text = cmdSessions(subargs || null); break; +``` + +- [ ] **Step 3: Update `cmdHelp` to include `sessions`** + +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: +```bash +cd /Users/taodeng/.openclaw/projects/memory-continuity && node -c mc-plugin/index.js +``` +Expected: no output (syntax OK) + +- [ ] **Step 5: Commit** + +```bash +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.json` to 3.1.0** + +Change `"version": "3.0.0"` to `"version": "3.1.0"`. + +- [ ] **Step 2: Bump version in `package.json` to 3.1.0** + +Change `"version"` value to `"3.1.0"`. + +- [ ] **Step 3: Verify both plugins 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 4: Commit and tag** + +```bash +git add openclaw.plugin.json package.json +git commit -m "release: v3.1.0 — short-term memory layer" +``` + +- [ ] **Step 5: Push to GitHub** + +```bash +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 | diff --git a/docs/superpowers/plans/2026-03-31-v3.2-mid-term-memory.md b/docs/superpowers/plans/2026-03-31-v3.2-mid-term-memory.md new file mode 100644 index 0000000..302bb36 --- /dev/null +++ b/docs/superpowers/plans/2026-03-31-v3.2-mid-term-memory.md @@ -0,0 +1,674 @@ +# 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 | diff --git a/docs/superpowers/plans/2026-03-31-v5.0-subagent-support.md b/docs/superpowers/plans/2026-03-31-v5.0-subagent-support.md new file mode 100644 index 0000000..293a019 --- /dev/null +++ b/docs/superpowers/plans/2026-03-31-v5.0-subagent-support.md @@ -0,0 +1,556 @@ +# Phase 5 — Conservative Subagent Support (v5.0) 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:** Enable working-state continuity across parent/child agent boundaries — parent seeds child with structured context, child recovers unsurfaced results back to parent. + +**Architecture:** Two new lifecycle hooks added to the existing plugin. (1) Enhance `before_agent_start` to detect forked child sessions and inject the parent's CURRENT_STATE.md as a structured seed. (2) Add a new `subagent_ended` hook that reads the child's workspace CURRENT_STATE.md and merges unsurfaced results into the parent's state. Both features are conservative — they degrade gracefully when context is unavailable and never overwrite parent state destructively. + +**Tech Stack:** Node.js ESM, OpenClaw plugin lifecycle hooks (`before_agent_start`, `subagent_ended`), filesystem-based state passing (cross-workspace read), zero external dependencies. + +**Research Findings (constraints):** +- `subagent_ended` event provides: `{ targetSessionKey, reason, outcome, error }` and context `{ childSessionKey, requesterSessionKey }` +- No pre-spawn hook exists — parent seeding happens via `before_agent_start` on the child side +- Workspaces are fully isolated — each agent has its own `memory/` directory +- Session key format: `agent::subagent:` — parseable to extract `agentId` +- Agent workspace is resolvable via config: `agents.list[].workspace` or default `~/.openclaw/workspace/main` +- `isSubagentSessionKey()` and `parseAgentSessionKey()` are runtime utilities (not available to plugins directly — must parse manually) + +--- + +## File Structure + +| Action | File | Responsibility | +|--------|------|----------------| +| Modify | `index.js` | Add parent-seed logic to `before_agent_start`, add new `subagent_ended` hook, add helper functions | +| Modify | `openclaw.plugin.json` | Bump to 5.0.0, add new config keys | +| Modify | `package.json` | Bump to 5.0.0 | + +--- + +### Task 1: Add Workspace Resolution Helpers + +**Files:** +- Modify: `index.js` (helpers section, before plugin definition) + +Two new helper functions are needed: one to detect if the current session is a subagent, and one to resolve a workspace directory from an agent ID. + +- [ ] **Step 1: Add `isSubagentSession` helper** + +In `index.js`, after the existing `extractStateFromMessages` function (around line 709) and before the `// Plugin Definition` comment, add: + +```javascript +/** + * Detect if the current session is a subagent by checking the session key format. + * Subagent session keys follow: agent::subagent: + */ +function isSubagentSession(ctx) { + const key = ctx?.sessionKey || ctx?.SessionKey || ""; + return /^agent:[^:]+:subagent:/.test(key); +} + +/** + * Extract the parent agent ID from a subagent session key. + * Session key format: agent::subagent: + * Returns null if not a subagent key. + */ +function parseParentAgentId(sessionKey) { + const match = sessionKey?.match(/^agent:([^:]+):subagent:/); + return match?.[1] || null; +} + +/** + * Resolve workspace directory for a given agent ID. + * Searches the OpenClaw config for agent workspace mappings. + * Falls back to ~/.openclaw/workspace/main for the "main" agent, + * or ~/.openclaw/workspaces/ for named agents. + */ +function resolveAgentWorkspace(agentId) { + if (!agentId) return null; + + const base = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "/tmp", ".openclaw"); + + // Try to read config for explicit workspace mapping + try { + const configPath = path.join(base, "openclaw.json"); + const config = JSON.parse(fs.readFileSync(configPath, "utf8")); + const agents = config?.agents?.list || []; + const entry = agents.find(a => a.id === agentId || a.name === agentId); + if (entry?.workspace) return entry.workspace; + + // Check defaults + if (agentId === "main") { + return config?.agents?.defaults?.workspace || path.join(base, "workspace", "main"); + } + } catch {} + + // Fallback heuristics + if (agentId === "main") return path.join(base, "workspace", "main"); + + // Named agents typically use ~/.openclaw/workspaces/ + const namedWs = path.join(base, "workspaces", agentId); + if (fs.existsSync(namedWs)) return namedWs; + + return null; +} + +/** + * Extract the Unsurfaced Results section from a state markdown file. + * Returns the raw text content of the section, or null if empty/placeholder. + */ +function extractUnsurfacedResults(md) { + if (!md) return null; + const section = extractSection(md, "Unsurfaced Results"); + if (!section || !isMeaningful(section)) return null; + return section; +} +``` + +- [ ] **Step 2: Verify the file still parses** + +```bash +cd /Users/taodeng/.openclaw/projects/memory-continuity +node -e "import('./index.js').then(m => console.log('OK:', typeof m.default))" +``` + +Expected: `OK: object` + +- [ ] **Step 3: Commit** + +```bash +git add index.js +git commit -m "feat(subagent): add workspace resolution and session detection helpers" +``` + +--- + +### Task 2: Parent→Child Seeding in before_agent_start + +**Files:** +- Modify: `index.js` (HOOK 1: before_agent_start) + +When a child agent starts (detected via subagent session key format), read the parent agent's CURRENT_STATE.md and inject it as additional context alongside the child's own state. + +- [ ] **Step 1: Enhance the `before_agent_start` hook** + +In `index.js`, find HOOK 1 (`before_agent_start`). The current code reads the agent's own state and injects it. After the existing relevance injection block (around the `if (config.relevanceInjection !== false)` block), add parent-seed logic: + +Find this existing code block: + +```javascript + // Relevance injection: find related historical entries + if (config.relevanceInjection !== false) { + const objective = extractSection(md, "Objective"); + const maxItems = config.maxRelevanceItems ?? 3; + const history = findRelevantHistory(ws, objective, maxItems); + if (history) { + parts.push(history); + log.info?.("[memory-continuity] Injected relevant history context"); + } + } +``` + +Right after this block (before the `return` statement), insert: + +```javascript + // Parent seed: if this is a subagent, inject parent's working state + if (config.subagentSeed !== false && isSubagentSession(_ctx)) { + try { + const sessionKey = _ctx?.sessionKey || _ctx?.SessionKey || ""; + const parentAgentId = parseParentAgentId(sessionKey); + if (parentAgentId) { + const parentWs = resolveAgentWorkspace(parentAgentId); + if (parentWs) { + const parentStatePath = resolveStatePath(parentWs); + const parentMd = parentStatePath ? readFile(parentStatePath) : null; + if (parentMd) { + const parentSnapshot = buildSnapshot(parentMd); + if (parentSnapshot) { + parts.push( + "=== PARENT AGENT CONTEXT ===\n" + + "The following is the parent agent's working state. " + + "Use this to understand the broader task context.\n" + + parentSnapshot + + "\n=== END PARENT CONTEXT ===" + ); + log.info?.("[memory-continuity] Injected parent state seed for subagent"); + } + } + } + } + } catch (err) { + log.warn?.("[memory-continuity] Parent seed failed (non-fatal): " + err.message); + } + } +``` + +- [ ] **Step 2: Verify plugin loads** + +```bash +cd /Users/taodeng/.openclaw/projects/memory-continuity +node -e "import('./index.js').then(m => console.log('OK:', typeof m.default))" +``` + +Expected: `OK: object` + +- [ ] **Step 3: Commit** + +```bash +git add index.js +git commit -m "feat(subagent): parent-to-child state seeding in before_agent_start" +``` + +--- + +### Task 3: Child→Parent Recovery via subagent_ended Hook + +**Files:** +- Modify: `index.js` (add new HOOK 6: subagent_ended) + +When a child agent ends, read its CURRENT_STATE.md and check for unsurfaced results. If found, append them to the parent's state file. + +- [ ] **Step 1: Add the `subagent_ended` hook** + +In `index.js`, find the section after HOOK 5 (`session_end`) and before the SERVICE section. Add a new hook between them: + +```javascript + // ------------------------------------------------------------------ + // HOOK 6: subagent_ended — recover child's unsurfaced results + // ------------------------------------------------------------------ + api.on("subagent_ended", async (event, _ctx) => { + const config = getConfig(); + if (config.subagentRecovery === false) return; + + const parentWs = _ctx?.workspaceDir; + if (!parentWs) return; + + const childSessionKey = event?.childSessionKey || _ctx?.childSessionKey; + if (!childSessionKey) { + log.info?.("[memory-continuity] subagent_ended: no childSessionKey, skipping"); + return; + } + + // Only process successful completions (not kills/errors) + const outcome = event?.outcome || ""; + const reason = event?.reason || ""; + if (outcome === "error" || reason === "killed" || reason === "spawn-failed") { + log.info?.("[memory-continuity] subagent_ended: child ended with " + (reason || outcome) + ", skipping recovery"); + return; + } + + try { + // Parse child agent ID from session key + const childMatch = childSessionKey.match(/^agent:([^:]+)/); + const childAgentId = childMatch?.[1]; + if (!childAgentId) return; + + const childWs = resolveAgentWorkspace(childAgentId); + if (!childWs) { + log.info?.("[memory-continuity] subagent_ended: cannot resolve child workspace for " + childAgentId); + return; + } + + // Read child's state + const childStatePath = resolveStatePath(childWs); + const childMd = childStatePath ? readFile(childStatePath) : null; + if (!childMd) return; + + // Extract unsurfaced results from child + const childUnsurfaced = extractUnsurfacedResults(childMd); + if (!childUnsurfaced) { + log.info?.("[memory-continuity] subagent_ended: no unsurfaced results in child state"); + return; + } + + // Also extract child's objective for context + const childObjective = extractSection(childMd, "Objective"); + + // Read parent's current state + const parentStatePath = resolveStatePath(parentWs); + if (!parentStatePath) return; + + let parentMd = readFile(parentStatePath); + if (!parentMd) { + parentMd = STATE_TEMPLATE; + } + + // Build the recovery note + const now = new Date().toISOString(); + const recoveryNote = [ + `[${now}] Subagent "${childAgentId}" completed.`, + childObjective ? ` Task: ${childObjective.split("\n")[0].slice(0, 120)}` : "", + ` Result: ${childUnsurfaced.split("\n")[0].slice(0, 200)}`, + ].filter(Boolean).join("\n"); + + // Merge into parent's Unsurfaced Results section + const existingUnsurfaced = extractSection(parentMd, "Unsurfaced Results"); + const mergedUnsurfaced = isMeaningful(existingUnsurfaced) + ? existingUnsurfaced + "\n" + recoveryNote + : recoveryNote; + + // Token-aware truncation of merged results + if (estimateTokens(mergedUnsurfaced) > 500) { + // Keep only the most recent entries (last 500 tokens) + const lines = mergedUnsurfaced.split("\n"); + let kept = []; + let tokens = 0; + for (let i = lines.length - 1; i >= 0; i--) { + const lineTokens = estimateTokens(lines[i]); + if (tokens + lineTokens > 500) break; + kept.unshift(lines[i]); + tokens += lineTokens; + } + const truncated = kept.join("\n"); + parentMd = parentMd.replace( + /## Unsurfaced Results\n[\s\S]*?(?=\n## |\n$|$)/, + "## Unsurfaced Results\n" + truncated + ); + } else { + parentMd = parentMd.replace( + /## Unsurfaced Results\n[\s\S]*?(?=\n## |\n$|$)/, + "## Unsurfaced Results\n" + mergedUnsurfaced + ); + } + + // Update the timestamp + parentMd = parentMd.replace( + /^> Last updated:.*$/m, + `> Last updated: ${now}` + ); + + writeFile(parentStatePath, parentMd); + log.info?.("[memory-continuity] Recovered unsurfaced results from subagent " + childAgentId); + } catch (err) { + log.warn?.("[memory-continuity] subagent_ended recovery failed (non-fatal): " + err.message); + } + }, { priority: 50 }); +``` + +- [ ] **Step 2: Verify plugin loads** + +```bash +cd /Users/taodeng/.openclaw/projects/memory-continuity +node -e "import('./index.js').then(m => console.log('OK:', typeof m.default))" +``` + +Expected: `OK: object` + +- [ ] **Step 3: Commit** + +```bash +git add index.js +git commit -m "feat(subagent): child-to-parent unsurfaced results recovery via subagent_ended" +``` + +--- + +### Task 4: New Config Keys + Manifest Bump + +**Files:** +- Modify: `openclaw.plugin.json` +- Modify: `package.json` + +- [ ] **Step 1: Add subagent config keys to `openclaw.plugin.json`** + +Bump version to `5.0.0`. Add two new config properties inside `configSchema.properties`: + +```json + "subagentSeed": { + "type": "boolean", + "default": true, + "description": "Inject parent working state into subagent context at startup" + }, + "subagentRecovery": { + "type": "boolean", + "default": true, + "description": "Recover unsurfaced results from completed subagents back to parent" + } +``` + +Add corresponding `uiHints`: + +```json + "subagentSeed": { + "label": "Subagent seeding", + "help": "Give subagents parent context so they understand the broader task" + }, + "subagentRecovery": { + "label": "Subagent recovery", + "help": "Pull completed subagent results back into parent state" + } +``` + +- [ ] **Step 2: Bump `package.json` to 5.0.0** + +Change `"version": "4.0.0"` to `"version": "5.0.0"`. + +- [ ] **Step 3: Verify JSON validity** + +```bash +cd /Users/taodeng/.openclaw/projects/memory-continuity +python3 -m json.tool openclaw.plugin.json > /dev/null && echo "JSON valid" +node -e "import('./index.js').then(m => console.log('OK:', typeof m.default))" +``` + +Expected: +``` +JSON valid +OK: object +``` + +- [ ] **Step 4: Commit** + +```bash +git add openclaw.plugin.json package.json +git commit -m "feat(subagent): add subagentSeed and subagentRecovery config keys, bump to v5.0.0" +``` + +--- + +### Task 5: Add `/mc subagents` Command + +**Files:** +- Modify: `mc-plugin/index.js` +- Modify: `mc-plugin/package.json` +- Modify: `mc-plugin/openclaw.plugin.json` + +Add a new `/mc subagents` command that shows subagent state across workspaces. + +- [ ] **Step 1: Add `cmdSubagents` function to mc-plugin** + +In `mc-plugin/index.js`, before the `cmdHelp` function, add: + +```javascript +function cmdSubagents(args) { + const agents = discoverAgents(); + if (agents.length === 0) return "No agents with memory found."; + + let out = "Subagent State Overview\n"; + out += "═══════════════════════\n\n"; + + for (const { name, memDir } of agents) { + const statePath = path.join(memDir, "CURRENT_STATE.md"); + const content = readFile(statePath); + if (!content) { + out += `[${name}] No state file\n\n`; + continue; + } + + const objMatch = content.match(/## Objective\n([\s\S]*?)(?=\n## )/); + const unsurfMatch = content.match(/## Unsurfaced Results\n([\s\S]*?)(?=\n## |$)/); + const updatedMatch = content.match(/^> Last updated:\s*(.+)$/m); + + const objective = objMatch?.[1]?.trim() || "None"; + const unsurfaced = unsurfMatch?.[1]?.trim() || "None"; + const updated = updatedMatch?.[1]?.trim() || "unknown"; + + out += `[${name}] Updated: ${updated}\n`; + out += ` Objective: ${truncate(objective.split("\n")[0], 80)}\n`; + if (unsurfaced !== "None") { + out += ` Unsurfaced: ${truncate(unsurfaced.split("\n")[0], 80)}\n`; + } + out += "\n"; + } + + return out.trimEnd(); +} +``` + +- [ ] **Step 2: Register the command in the switch statement** + +Find the `switch (subcmd)` block and add a new case before the `help` case: + +```javascript + case "subagents": text = cmdSubagents(subargs || null); break; +``` + +- [ ] **Step 3: Update help text** + +In `cmdHelp`, add: + +``` +/mc subagents Subagent state overview across workspaces +``` + +- [ ] **Step 4: Bump mc-plugin versions to 2.1.0** + +In `mc-plugin/package.json`, change version to `"2.1.0"`. +In `mc-plugin/openclaw.plugin.json`, change version to `"2.1.0"`. + +- [ ] **Step 5: Verify mc-plugin loads** + +```bash +cd /Users/taodeng/.openclaw/projects/memory-continuity/mc-plugin +node -e "import('./index.js').then(m => console.log('OK:', typeof m.default))" +``` + +Expected: `OK: function` + +- [ ] **Step 6: Commit** + +```bash +git add mc-plugin/ +git commit -m "feat(subagent): add /mc subagents command for cross-workspace state view" +``` + +--- + +### Task 6: Version Verify + Git + Deploy + +**Files:** All modified files + +- [ ] **Step 1: Verify all versions** + +```bash +grep '"version"' openclaw.plugin.json package.json mc-plugin/openclaw.plugin.json mc-plugin/package.json +``` + +Expected: +``` +openclaw.plugin.json: "version": "5.0.0" +package.json: "version": "5.0.0" +mc-plugin/openclaw.plugin.json: "version": "2.1.0" +mc-plugin/package.json: "version": "2.1.0" +``` + +- [ ] **Step 2: Final commit + tag + push** + +```bash +git add -A +git commit -m "release: v5.0.0 — conservative subagent support (parent seed + child recovery)" +git tag v5.0.0 +git push origin main --tags +``` + +- [ ] **Step 3: Deploy to Cloud** + +```bash +scp index.js package.json openclaw.plugin.json opc@152.67.121.35:~/.openclaw/extensions/memory-continuity/ +scp mc-plugin/index.js mc-plugin/package.json mc-plugin/openclaw.plugin.json opc@152.67.121.35:~/.openclaw/extensions/mc/ +ssh opc@152.67.121.35 "pkill -f 'openclaw.*gateway' 2>/dev/null; sleep 2; nohup openclaw gateway start > /dev/null 2>&1 &" +``` + +- [ ] **Step 4: Deploy to PI** + +```bash +scp index.js package.json openclaw.plugin.json administrator@172.16.2.232:~/.openclaw/extensions/memory-continuity/ +scp mc-plugin/index.js mc-plugin/package.json mc-plugin/openclaw.plugin.json administrator@172.16.2.232:~/.openclaw/extensions/mc/ +ssh administrator@172.16.2.232 "pkill -f 'openclaw.*gateway' 2>/dev/null; sleep 2; nohup openclaw gateway start > /dev/null 2>&1 &" +``` + +- [ ] **Step 5: Deploy to Mac** + +```bash +cp index.js package.json openclaw.plugin.json ~/.openclaw/extensions/memory-continuity/ +pkill -f 'openclaw.*gateway' 2>/dev/null; sleep 2; nohup openclaw gateway start > /dev/null 2>&1 & +``` + +- [ ] **Step 6: Verify all 3 servers** + +```bash +grep version ~/.openclaw/extensions/memory-continuity/package.json +ssh opc@152.67.121.35 "grep version ~/.openclaw/extensions/memory-continuity/package.json" +ssh administrator@172.16.2.232 "grep version ~/.openclaw/extensions/memory-continuity/package.json" +``` + +Expected: all show `"version": "5.0.0"`. diff --git a/index.js b/index.js index 7694040..0fbc365 100644 --- a/index.js +++ b/index.js @@ -708,6 +708,71 @@ ${truncate(lastAssistant, 500)} `; } +/** + * Detect if the current session is a subagent by checking the session key format. + * Subagent session keys follow: agent::subagent: + */ +function isSubagentSession(ctx) { + const key = ctx?.sessionKey || ctx?.SessionKey || ""; + return /^agent:[^:]+:subagent:/.test(key); +} + +/** + * Extract the parent agent ID from a subagent session key. + * Session key format: agent::subagent: + * Returns null if not a subagent key. + */ +function parseParentAgentId(sessionKey) { + const match = sessionKey?.match(/^agent:([^:]+):subagent:/); + return match?.[1] || null; +} + +/** + * Resolve workspace directory for a given agent ID. + * Searches the OpenClaw config for agent workspace mappings. + * Falls back to ~/.openclaw/workspace/main for the "main" agent, + * or ~/.openclaw/workspaces/ for named agents. + */ +function resolveAgentWorkspace(agentId) { + if (!agentId) return null; + + const base = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "/tmp", ".openclaw"); + + // Try to read config for explicit workspace mapping + try { + const configPath = path.join(base, "openclaw.json"); + const config = JSON.parse(fs.readFileSync(configPath, "utf8")); + const agents = config?.agents?.list || []; + const entry = agents.find(a => a.id === agentId || a.name === agentId); + if (entry?.workspace) return entry.workspace; + + // Check defaults + if (agentId === "main") { + return config?.agents?.defaults?.workspace || path.join(base, "workspace", "main"); + } + } catch {} + + // Fallback heuristics + if (agentId === "main") return path.join(base, "workspace", "main"); + + // Named agents typically use ~/.openclaw/workspaces/ + const namedWs = path.join(base, "workspaces", agentId); + if (fs.existsSync(namedWs)) return namedWs; + + return null; +} + +/** + * Extract the Unsurfaced Results section from a state markdown file. + * Returns the raw text content of the section, or null if empty/placeholder. + */ +function extractUnsurfacedResults(md) { + if (!md) return null; + const section = extractSection(md, "Unsurfaced Results"); + if (!section || !isMeaningful(section)) return null; + return section; +} + // --------------------------------------------------------------------------- // Plugin Definition // --------------------------------------------------------------------------- @@ -750,6 +815,36 @@ const plugin = { } } + // Parent seed: if this is a subagent, inject parent's working state + if (config.subagentSeed !== false && isSubagentSession(_ctx)) { + try { + const sessionKey = _ctx?.sessionKey || _ctx?.SessionKey || ""; + const parentAgentId = parseParentAgentId(sessionKey); + if (parentAgentId) { + const parentWs = resolveAgentWorkspace(parentAgentId); + if (parentWs) { + const parentStatePath = resolveStatePath(parentWs); + const parentMd = parentStatePath ? readFile(parentStatePath) : null; + if (parentMd) { + const parentSnapshot = buildSnapshot(parentMd); + if (parentSnapshot) { + parts.push( + "=== PARENT AGENT CONTEXT ===\n" + + "The following is the parent agent's working state. " + + "Use this to understand the broader task context.\n" + + parentSnapshot + + "\n=== END PARENT CONTEXT ===" + ); + log.info?.("[memory-continuity] Injected parent state seed for subagent"); + } + } + } + } + } catch (err) { + log.warn?.("[memory-continuity] Parent seed failed (non-fatal): " + err.message); + } + } + return { prependSystemContext: parts.join("\n\n") + "\n\n" + @@ -896,6 +991,116 @@ const plugin = { } }, { priority: 90 }); + // ------------------------------------------------------------------ + // HOOK 6: subagent_ended — recover child's unsurfaced results + // ------------------------------------------------------------------ + api.on("subagent_ended", async (event, _ctx) => { + const config = getConfig(); + if (config.subagentRecovery === false) return; + + const parentWs = _ctx?.workspaceDir; + if (!parentWs) return; + + const childSessionKey = event?.childSessionKey || _ctx?.childSessionKey; + if (!childSessionKey) { + log.info?.("[memory-continuity] subagent_ended: no childSessionKey, skipping"); + return; + } + + // Only process successful completions (not kills/errors) + const outcome = event?.outcome || ""; + const reason = event?.reason || ""; + if (outcome === "error" || reason === "killed" || reason === "spawn-failed") { + log.info?.("[memory-continuity] subagent_ended: child ended with " + (reason || outcome) + ", skipping recovery"); + return; + } + + try { + // Parse child agent ID from session key + const childMatch = childSessionKey.match(/^agent:([^:]+)/); + const childAgentId = childMatch?.[1]; + if (!childAgentId) return; + + const childWs = resolveAgentWorkspace(childAgentId); + if (!childWs) { + log.info?.("[memory-continuity] subagent_ended: cannot resolve child workspace for " + childAgentId); + return; + } + + // Read child's state + const childStatePath = resolveStatePath(childWs); + const childMd = childStatePath ? readFile(childStatePath) : null; + if (!childMd) return; + + // Extract unsurfaced results from child + const childUnsurfaced = extractUnsurfacedResults(childMd); + if (!childUnsurfaced) { + log.info?.("[memory-continuity] subagent_ended: no unsurfaced results in child state"); + return; + } + + // Also extract child's objective for context + const childObjective = extractSection(childMd, "Objective"); + + // Read parent's current state + const parentStatePath = resolveStatePath(parentWs); + if (!parentStatePath) return; + + let parentMd = readFile(parentStatePath); + if (!parentMd) { + parentMd = STATE_TEMPLATE; + } + + // Build the recovery note + const now = new Date().toISOString(); + const recoveryNote = [ + `[${now}] Subagent "${childAgentId}" completed.`, + childObjective ? ` Task: ${childObjective.split("\n")[0].slice(0, 120)}` : "", + ` Result: ${childUnsurfaced.split("\n")[0].slice(0, 200)}`, + ].filter(Boolean).join("\n"); + + // Merge into parent's Unsurfaced Results section + const existingUnsurfaced = extractSection(parentMd, "Unsurfaced Results"); + const mergedUnsurfaced = isMeaningful(existingUnsurfaced) + ? existingUnsurfaced + "\n" + recoveryNote + : recoveryNote; + + // Token-aware truncation of merged results + if (estimateTokens(mergedUnsurfaced) > 500) { + const lines = mergedUnsurfaced.split("\n"); + let kept = []; + let tokens = 0; + for (let i = lines.length - 1; i >= 0; i--) { + const lineTokens = estimateTokens(lines[i]); + if (tokens + lineTokens > 500) break; + kept.unshift(lines[i]); + tokens += lineTokens; + } + const truncated = kept.join("\n"); + parentMd = parentMd.replace( + /## Unsurfaced Results\n[\s\S]*?(?=\n## |\n$|$)/, + "## Unsurfaced Results\n" + truncated + ); + } else { + parentMd = parentMd.replace( + /## Unsurfaced Results\n[\s\S]*?(?=\n## |\n$|$)/, + "## Unsurfaced Results\n" + mergedUnsurfaced + ); + } + + // Update the timestamp + parentMd = parentMd.replace( + /^> Last updated:.*$/m, + `> Last updated: ${now}` + ); + + writeFile(parentStatePath, parentMd); + log.info?.("[memory-continuity] Recovered unsurfaced results from subagent " + childAgentId); + } catch (err) { + log.warn?.("[memory-continuity] subagent_ended recovery failed (non-fatal): " + err.message); + } + }, { priority: 50 }); + // ------------------------------------------------------------------ // SERVICE: mc:recall — programmatic interface for other plugins // ------------------------------------------------------------------ diff --git a/mc-plugin/index.js b/mc-plugin/index.js index 94c2ad8..0e56ae1 100644 --- a/mc-plugin/index.js +++ b/mc-plugin/index.js @@ -780,6 +780,40 @@ function cmdRecall(args) { return out.trimEnd(); } +function cmdSubagents(args) { + const agents = discoverAgents(); + if (agents.length === 0) return "No agents with memory found."; + + let out = "Subagent State Overview\n"; + out += "═══════════════════════\n\n"; + + for (const { name, memDir } of agents) { + const statePath = path.join(memDir, "CURRENT_STATE.md"); + const content = readFile(statePath); + if (!content) { + out += `[${name}] No state file\n\n`; + continue; + } + + const objMatch = content.match(/## Objective\n([\s\S]*?)(?=\n## )/); + const unsurfMatch = content.match(/## Unsurfaced Results\n([\s\S]*?)(?=\n## |$)/); + const updatedMatch = content.match(/^> Last updated:\s*(.+)$/m); + + const objective = objMatch?.[1]?.trim() || "None"; + const unsurfaced = unsurfMatch?.[1]?.trim() || "None"; + const updated = updatedMatch?.[1]?.trim() || "unknown"; + + out += `[${name}] Updated: ${updated}\n`; + out += ` Objective: ${truncate(objective.split("\n")[0], 80)}\n`; + if (unsurfaced !== "None") { + out += ` Unsurfaced: ${truncate(unsurfaced.split("\n")[0], 80)}\n`; + } + out += "\n"; + } + + return out.trimEnd(); +} + function cmdHelp() { return `MC Commands (Memory Continuity) ───────────────────────────── @@ -796,7 +830,8 @@ function cmdHelp() { /mc settings Update a setting /mc compact [agent] Compress state file /mc export [agent|all] Export (--from/--to/--tag) -/mc recall Find relevant history by topic`; +/mc recall Find relevant history by topic +/mc subagents Subagent state overview across workspaces`; } // ── Plugin entry point ────────────────────────────────────────────────── @@ -831,6 +866,7 @@ export default function (api) { case "tags": text = cmdTags(subargs || null); break; case "summary": text = cmdSummary(subargs || null); break; case "recall": text = cmdRecall(subargs); break; + case "subagents": text = cmdSubagents(subargs || null); break; case "help": case "--help": case "-h": case "": text = cmdHelp(); break; default: diff --git a/mc-plugin/openclaw.plugin.json b/mc-plugin/openclaw.plugin.json index afa7657..60f3474 100644 --- a/mc-plugin/openclaw.plugin.json +++ b/mc-plugin/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "mc", "name": "Memory Continuity Commands", "description": "Slash commands for Memory Continuity — /mc state, /mc history, /mc search, /mc settings, etc.", - "version": "2.0.0", + "version": "2.1.0", "author": { "name": "dtzp555-max", "url": "https://github.com/dtzp555-max" }, "license": "MIT", "category": "commands", diff --git a/mc-plugin/package.json b/mc-plugin/package.json index 2731242..dc5886a 100644 --- a/mc-plugin/package.json +++ b/mc-plugin/package.json @@ -1,6 +1,6 @@ { "name": "mc-plugin", - "version": "2.0.0", + "version": "2.1.0", "description": "Slash commands for Memory Continuity — /mc state, /mc history, /mc search, etc.", "main": "index.js", "type": "module", diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 03370e6..0ef9925 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "memory-continuity", "name": "Memory Continuity", "description": "Preserves working state across /new, reset, compaction, and gateway restarts via a simple markdown checkpoint file.", - "version": "4.0.0", + "version": "5.0.0", "author": { "name": "dtzp555-max", "url": "https://github.com/dtzp555-max" }, "license": "MIT", "repository": "https://github.com/dtzp555-max/memory-continuity", @@ -80,6 +80,16 @@ "type": "number", "default": 30, "description": "Days before archives move to cold storage (0 = disabled)" + }, + "subagentSeed": { + "type": "boolean", + "default": true, + "description": "Inject parent working state into subagent context at startup" + }, + "subagentRecovery": { + "type": "boolean", + "default": true, + "description": "Recover unsurfaced results from completed subagents back to parent" } } }, @@ -127,6 +137,14 @@ "archiveDecayDays": { "label": "Archive decay days", "help": "Move archives older than N days to cold/ subfolder (0 to disable)" + }, + "subagentSeed": { + "label": "Subagent seeding", + "help": "Give subagents parent context so they understand the broader task" + }, + "subagentRecovery": { + "label": "Subagent recovery", + "help": "Pull completed subagent results back into parent state" } } } \ No newline at end of file diff --git a/package.json b/package.json index c185c61..0f47415 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "memory-continuity", - "version": "4.0.0", + "version": "5.0.0", "description": "Zero-dependency memory continuity for OpenClaw — plain markdown, lifecycle hooks, no vector DB.", "main": "index.js", "type": "module",