fix: retire one-shot agent MCP runtimes

This commit is contained in:
Peter Steinberger
2026-04-25 08:57:56 +01:00
parent 8fd15ed0e5
commit e0bee76fb0
6 changed files with 17 additions and 3 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- MCP/CLI: retire bundled MCP runtimes at the end of one-shot `openclaw agent` and `openclaw infer model run` gateway/local executions, so repeated scripted runs do not accumulate stdio MCP child processes. Fixes #71457.
- OpenAI/Codex image generation: canonicalize legacy `openai-codex.baseUrl` values such as `https://chatgpt.com/backend-api` to the Codex Responses backend before calling `gpt-image-2`, matching the chat transport. Fixes #71460.
- Control UI: make `/usage` use the fresh context snapshot for context percentage, and include cache-write tokens in the Usage overview cache-hit denominator. Fixes #47885. Thanks @imwyvern and @Ante042.
- GitHub Copilot: preserve encrypted Responses reasoning item IDs during replay

View File

@@ -52,6 +52,7 @@ openclaw agent --agent ops --message "Run locally" --local
- Gateway mode falls back to the embedded agent when the Gateway request fails. Use `--local` to force embedded execution up front.
- `--local` still preloads the plugin registry first, so plugin-provided providers, tools, and channels stay available during embedded runs.
- Each `openclaw agent` invocation is treated as a one-shot run. Bundled or user-configured MCP servers opened for that run are retired after the reply, even when the command uses the Gateway path, so stdio MCP child processes do not stay alive between scripted invocations.
- `--channel`, `--reply-channel`, and `--reply-account` affect reply delivery, not session routing.
- `--json` keeps stdout reserved for the JSON response. Gateway, plugin, and embedded-fallback diagnostics are routed to stderr so scripts can parse stdout directly.
- When this command triggers `models.json` regeneration, SecretRef-managed provider credentials are persisted as non-secret markers (for example env var names, `secretref-env:ENV_VAR_NAME`, or `secretref-managed`), not resolved secret plaintext.

View File

@@ -130,6 +130,7 @@ This table maps common inference tasks to the corresponding infer command.
- Stateless execution commands default to local.
- Gateway-managed state commands default to gateway.
- The normal local path does not require the gateway to be running.
- `model run` is one-shot. MCP servers opened through the agent runtime for that command are retired after the reply for both local and `--gateway` execution, so repeated scripted invocations do not keep stdio MCP child processes alive.
## Model
@@ -145,6 +146,7 @@ openclaw infer model inspect --name gpt-5.5 --json
Notes:
- `model run` reuses the agent runtime so provider/model overrides behave like normal agent execution.
- Because `model run` is intended for headless automation, it does not retain per-session bundled MCP runtimes after the command finishes.
- `model auth login`, `model auth logout`, and `model auth status` manage saved provider auth state.
## Image

View File

@@ -61,6 +61,10 @@ Important behavior:
- older transcript history is read with `messages_read`
- Claude push notifications only exist while the MCP session is alive
- when the client disconnects, the bridge exits and the live queue is gone
- one-shot agent entry points such as `openclaw agent` and
`openclaw infer model run` retire any bundled MCP runtimes they open when the
reply completes, so repeated scripted runs do not accumulate stdio MCP child
processes
- stdio MCP servers launched by OpenClaw (bundled or user-configured) are torn
down as a process tree on shutdown, so child subprocesses started by the
server do not survive after the parent stdio client exits

View File

@@ -117,6 +117,11 @@ describe("agentCliCommand", () => {
await agentCliCommand({ message: "hi", to: "+1555" }, runtime);
expect(callGateway).toHaveBeenCalledTimes(1);
expect(callGateway.mock.calls[0]?.[0]).toMatchObject({
params: {
cleanupBundleMcpOnRunEnd: true,
},
});
expect(agentCommand).not.toHaveBeenCalled();
expect(runtime.log).toHaveBeenCalledWith("hello");
});
@@ -198,7 +203,7 @@ describe("agentCliCommand", () => {
});
});
it("does not force bundle MCP cleanup on gateway fallback", async () => {
it("forces bundle MCP cleanup on embedded fallback", async () => {
await withTempStore(async () => {
callGateway.mockRejectedValue(new Error("gateway not connected"));
mockLocalAgentReply();
@@ -206,7 +211,7 @@ describe("agentCliCommand", () => {
await agentCliCommand({ message: "hi", to: "+1555" }, runtime);
expect(agentCommand).toHaveBeenCalledTimes(1);
expect(agentCommand.mock.calls[0]?.[0]).not.toMatchObject({
expect(agentCommand.mock.calls[0]?.[0]).toMatchObject({
cleanupBundleMcpOnRunEnd: true,
});
});

View File

@@ -152,6 +152,7 @@ export async function agentViaGatewayCommand(opts: AgentCliOpts, runtime: Runtim
bestEffortDeliver: opts.bestEffortDeliver,
timeout: timeoutSeconds,
lane: opts.lane,
cleanupBundleMcpOnRunEnd: true,
extraSystemPrompt: opts.extraSystemPrompt,
idempotencyKey,
},
@@ -191,7 +192,7 @@ export async function agentCliCommand(opts: AgentCliOpts, runtime: RuntimeEnv, d
...opts,
agentId: opts.agent,
replyAccountId: opts.replyAccount,
cleanupBundleMcpOnRunEnd: opts.local === true,
cleanupBundleMcpOnRunEnd: true,
};
if (opts.local === true) {
return await agentCommand(localOpts, runtime, deps);