Summary
When cron jobs use sessionTarget: "isolated" with payload.kind: "agentTurn", each run spawns a full set of bundle MCP stdio child processes (one per configured MCP server), but these processes are never released after the run completes. This causes unbounded process accumulation and memory growth.
Version
OpenClaw 2026.4.15 (041266a)
Reproduction
- Configure multiple MCP servers in
openclaw.json (e.g. searxng, crawl4ai, filesystem, memory, sqlite, git, context7, sequential-thinking, time)
- Create a cron job with
sessionTarget: "isolated" and payload.kind: "agentTurn"
- Set it to run frequently (e.g.
*/3 * * * *)
- Observe: each cron run spawns ~9 new Node.js child processes (one per MCP server), each consuming ~100 MB RSS
- After N runs: N × (num_mcp_servers) orphan processes, never garbage-collected
Root Cause Analysis
After tracing the code paths in the dist/ bundle:
-
MCP runtime creation (pi-bundle-mcp-tools-Diapg7vD.js):
getOrCreateSessionMcpRuntime() creates a SessionMcpRuntime that spawns stdio child processes for every configured MCP server
- Runtime is keyed by
sessionId and cached in runtimesBySessionId Map
disposeSessionMcpRuntime(sessionId) properly kills all child processes
-
Cron execution path (server.impl-STMC_0uD.js → pi-embedded-runner-CefZK1Pt.js):
- Each
isolated cron job creates a new session with a unique sessionId
runEmbeddedPiAgent() calls getOrCreateSessionMcpRuntime() → spawns all MCP processes
runEmbeddedPiAgent() receives cleanupBundleMcpOnRunEnd = undefined (never set to true)
- The
finally block at line 9144 only disposes runtime when cleanupBundleMcpOnRunEnd === true
-
Reply path (get-reply-Bt3IWYIA.js):
disposeSessionMcpRuntime(previousSessionEntry.sessionId) is only called when a session reset occurs
- Normal message turns reuse the same session → no new MCP processes spawned
- But isolated cron sessions are always new → always spawn new processes
-
Only the CLI --local path (register.agent-9kfgbSik.js line 115) sets cleanupBundleMcpOnRunEnd: true
Impact
With 9 MCP servers and a */3 * * * isolated cron job:
- Each run: ~9 new Node processes × ~100 MB RSS = ~900 MB
- After 1 hour (20 runs): ~180 processes, ~18 GB RSS
- Processes accumulate indefinitely; only a gateway restart clears them
Evidence
On a live system after ~10 minutes of uptime:
searxng-mcp: 6 instances (~580 MB total)
context7-mcp: 6 instances (~600 MB total)
git-mcp-server: 6 instances (~870 MB total)
playwright/mcp Docker containers: 6 instances
github-mcp-server Docker containers: 6 instances
- All other MCP servers: 6 instances each
- Session store: 58 entries, each with unique sessionId
Expected Behavior
- Cron runs with
sessionTarget: "isolated" should release MCP runtimes after completion
- OR: isolated cron sessions should reuse a single MCP runtime keyed by job ID, not session ID
- Session maintenance prune/cap should also dispose the associated MCP runtimes
Suggested Fixes
- Pass
cleanupBundleMcpOnRunEnd: true in the cron execution path (server.impl-STMC_0uD.js) so the finally block in runEmbeddedPiAgent disposes the runtime
- Dispose runtimes during session maintenance when entries are pruned or capped
- Cache isolated session runtimes by cron job ID instead of sessionId to reuse processes across runs
Summary
When cron jobs use
sessionTarget: "isolated"withpayload.kind: "agentTurn", each run spawns a full set of bundle MCP stdio child processes (one per configured MCP server), but these processes are never released after the run completes. This causes unbounded process accumulation and memory growth.Version
OpenClaw 2026.4.15 (041266a)
Reproduction
openclaw.json(e.g. searxng, crawl4ai, filesystem, memory, sqlite, git, context7, sequential-thinking, time)sessionTarget: "isolated"andpayload.kind: "agentTurn"*/3 * * * *)Root Cause Analysis
After tracing the code paths in the dist/ bundle:
MCP runtime creation (
pi-bundle-mcp-tools-Diapg7vD.js):getOrCreateSessionMcpRuntime()creates aSessionMcpRuntimethat spawns stdio child processes for every configured MCP serversessionIdand cached inruntimesBySessionIdMapdisposeSessionMcpRuntime(sessionId)properly kills all child processesCron execution path (
server.impl-STMC_0uD.js→pi-embedded-runner-CefZK1Pt.js):isolatedcron job creates a new session with a uniquesessionIdrunEmbeddedPiAgent()callsgetOrCreateSessionMcpRuntime()→ spawns all MCP processesrunEmbeddedPiAgent()receivescleanupBundleMcpOnRunEnd = undefined(never set totrue)finallyblock at line 9144 only disposes runtime whencleanupBundleMcpOnRunEnd === trueReply path (
get-reply-Bt3IWYIA.js):disposeSessionMcpRuntime(previousSessionEntry.sessionId)is only called when a session reset occursOnly the CLI
--localpath (register.agent-9kfgbSik.jsline 115) setscleanupBundleMcpOnRunEnd: trueImpact
With 9 MCP servers and a
*/3 * * *isolated cron job:Evidence
On a live system after ~10 minutes of uptime:
searxng-mcp: 6 instances (~580 MB total)context7-mcp: 6 instances (~600 MB total)git-mcp-server: 6 instances (~870 MB total)playwright/mcpDocker containers: 6 instancesgithub-mcp-serverDocker containers: 6 instancesExpected Behavior
sessionTarget: "isolated"should release MCP runtimes after completionSuggested Fixes
cleanupBundleMcpOnRunEnd: truein the cron execution path (server.impl-STMC_0uD.js) so thefinallyblock inrunEmbeddedPiAgentdisposes the runtime