- 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>
19 KiB
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_endedevent provides:{ targetSessionKey, reason, outcome, error }and context{ childSessionKey, requesterSessionKey }- No pre-spawn hook exists — parent seeding happens via
before_agent_starton the child side - Workspaces are fully isolated — each agent has its own
memory/directory - Session key format:
agent:<agentId>:subagent:<uuid>— parseable to extractagentId - Agent workspace is resolvable via config:
agents.list[].workspaceor default~/.openclaw/workspace/main isSubagentSessionKey()andparseAgentSessionKey()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
isSubagentSessionhelper
In index.js, after the existing extractStateFromMessages function (around line 709) and before the // Plugin Definition comment, add:
/**
* Detect if the current session is a subagent by checking the session key format.
* Subagent session keys follow: agent:<agentId>:subagent:<uuid>
*/
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:<parentAgentId>:subagent:<uuid>
* 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/<agentId> 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/<agentId>
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
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
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_starthook
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:
// 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:
// 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
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
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_endedhook
In index.js, find the section after HOOK 5 (session_end) and before the SERVICE section. Add a new hook between them:
// ------------------------------------------------------------------
// 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
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
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:
"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:
"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.jsonto 5.0.0
Change "version": "4.0.0" to "version": "5.0.0".
- Step 3: Verify JSON validity
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
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
cmdSubagentsfunction to mc-plugin
In mc-plugin/index.js, before the cmdHelp function, add:
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:
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
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
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
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
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
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
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
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
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".