# 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"`.