diff --git a/CHANGELOG.md b/CHANGELOG.md index 62fd06a5e69..4a76e13c8b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai - Providers/OpenAI: lock the auth picker wording for OpenAI API key, Codex browser login, and Codex device pairing so the setup choices no longer imply a mixed Codex/API-key auth path. (#67848) Thanks @tmlxrd. - Agents/BTW: route `/btw` side questions through provider stream registration with the session workspace, so Ollama provider URL construction and workspace-scoped hooks apply correctly. Fixes #68336. (#70413) Thanks @suboss87. - Agents/sessions: make session transcript write locks non-reentrant by default, so same-process transcript writers contend unless a helper explicitly opts into nested lock ownership. +- ACPX/probe: expose an optional `probeAgent` plugin config field so the embedded ACP runtime health probe can target a configured agent (for example `opencode` or `claude`) instead of hardcoding `codex`, and stop marking the entire ACP runtime backend unavailable when the default probe agent is simply not installed or not authenticated. (#68409) Thanks @lyfuci. - Memory search: use sqlite-vec KNN for vector recall while preserving full post-filter result limits in multi-model indexes. Fixes #69666. (#69680) Thanks @aalekh-sarvam. - Providers/OpenAI Codex: stop stale per-agent `openai-codex:default` OAuth profiles from shadowing a newer main-agent identity-scoped profile, and let `openclaw doctor` offer the matching cleanup. (#70393) Thanks @pashpashpash. - ACPX: route OpenClaw ACP bridge commands through the MCP-free runtime path even when the command is wrapped with `env`, has bridge flags, or is resumed from persisted session state, so documented `acpx openclaw` setups no longer fail on per-session MCP injection. (#68741) Thanks @alexlomt. diff --git a/extensions/acpx/openclaw.plugin.json b/extensions/acpx/openclaw.plugin.json index f05107a57fa..416eeb84d83 100644 --- a/extensions/acpx/openclaw.plugin.json +++ b/extensions/acpx/openclaw.plugin.json @@ -46,6 +46,10 @@ "type": "number", "minimum": 0 }, + "probeAgent": { + "type": "string", + "minLength": 1 + }, "mcpServers": { "type": "object", "additionalProperties": { @@ -132,6 +136,11 @@ "help": "Reserved compatibility field for the older embedded ACPX queue-owner path. Accepted for compatibility and logged as ignored.", "advanced": true }, + "probeAgent": { + "label": "Probe Agent", + "help": "Agent id used for the embedded ACP runtime health probe. Defaults to the runtime built-in probe agent (codex). Set this to another configured ACP agent id (for example `opencode` or `claude`) when the default probe agent is not installed or not authenticated, so the whole embedded ACP backend does not get marked unavailable.", + "advanced": true + }, "mcpServers": { "label": "MCP Servers", "help": "Named MCP server definitions to inject into embedded ACP session bootstrap. Each entry needs a command and can include args and env.", diff --git a/extensions/acpx/src/config-schema.ts b/extensions/acpx/src/config-schema.ts index 0b390363d51..306f58f0cc7 100644 --- a/extensions/acpx/src/config-schema.ts +++ b/extensions/acpx/src/config-schema.ts @@ -34,6 +34,7 @@ export type AcpxPluginConfig = { strictWindowsCmdWrapper?: boolean; timeoutSeconds?: number; queueOwnerTtlSeconds?: number; + probeAgent?: string; mcpServers?: Record; agents?: Record; }; @@ -49,6 +50,7 @@ export type ResolvedAcpxPluginConfig = { strictWindowsCmdWrapper: boolean; timeoutSeconds?: number; queueOwnerTtlSeconds: number; + probeAgent?: string; legacyCompatibilityConfig: { strictWindowsCmdWrapper?: boolean; queueOwnerTtlSeconds?: number; @@ -107,6 +109,7 @@ export const AcpxPluginConfigSchema = z.strictObject({ .number({ error: "queueOwnerTtlSeconds must be a number >= 0" }) .min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" }) .optional(), + probeAgent: nonEmptyTrimmedString("probeAgent must be a non-empty string").optional(), mcpServers: z.record(z.string(), McpServerConfigSchema).optional(), agents: z .record( diff --git a/extensions/acpx/src/config.test.ts b/extensions/acpx/src/config.test.ts index 8f25a9df5f8..68aa4a156ab 100644 --- a/extensions/acpx/src/config.test.ts +++ b/extensions/acpx/src/config.test.ts @@ -58,6 +58,37 @@ describe("embedded acpx plugin config", () => { }); }); + it("leaves probeAgent undefined by default so the runtime picks its built-in probe agent", () => { + const resolved = resolveAcpxPluginConfig({ + rawConfig: undefined, + workspaceDir: "/tmp/openclaw-acpx", + }); + + expect(resolved.probeAgent).toBeUndefined(); + }); + + it("carries an explicit probeAgent through to the resolved plugin config, trimmed", () => { + const resolved = resolveAcpxPluginConfig({ + rawConfig: { + probeAgent: " opencode ", + }, + workspaceDir: "/tmp/openclaw-acpx", + }); + + expect(resolved.probeAgent).toBe("opencode"); + }); + + it("rejects an empty probeAgent string", () => { + expect(() => + resolveAcpxPluginConfig({ + rawConfig: { + probeAgent: "", + }, + workspaceDir: "/tmp/openclaw-acpx", + }), + ).toThrow(/probeAgent must be a non-empty string/); + }); + it("injects the built-in plugin-tools MCP server only when explicitly enabled", () => { const resolved = resolveAcpxPluginConfig({ rawConfig: { diff --git a/extensions/acpx/src/config.ts b/extensions/acpx/src/config.ts index c94380aa896..a77a36e117c 100644 --- a/extensions/acpx/src/config.ts +++ b/extensions/acpx/src/config.ts @@ -260,6 +260,8 @@ export function resolveAcpxPluginConfig(params: { ]), ); + const probeAgent = normalized.probeAgent?.trim(); + return { cwd, stateDir, @@ -273,6 +275,7 @@ export function resolveAcpxPluginConfig(params: { normalized.strictWindowsCmdWrapper ?? DEFAULT_STRICT_WINDOWS_CMD_WRAPPER, timeoutSeconds: normalized.timeoutSeconds ?? DEFAULT_ACPX_TIMEOUT_SECONDS, queueOwnerTtlSeconds: normalized.queueOwnerTtlSeconds ?? DEFAULT_QUEUE_OWNER_TTL_SECONDS, + probeAgent: probeAgent && probeAgent.length > 0 ? probeAgent : undefined, legacyCompatibilityConfig: { strictWindowsCmdWrapper: normalized.strictWindowsCmdWrapper, queueOwnerTtlSeconds: normalized.queueOwnerTtlSeconds, diff --git a/extensions/acpx/src/service.test.ts b/extensions/acpx/src/service.test.ts index bc95bd1313c..63ef39887d7 100644 --- a/extensions/acpx/src/service.test.ts +++ b/extensions/acpx/src/service.test.ts @@ -145,6 +145,37 @@ describe("createAcpxRuntimeService", () => { await service.stop?.(ctx); }); + it("forwards a configured probeAgent to the runtime factory so the probe does not hardcode the default", async () => { + const workspaceDir = await makeTempDir(); + const ctx = createServiceContext(workspaceDir); + const runtime = { + ensureSession: vi.fn(), + runTurn: vi.fn(), + cancel: vi.fn(), + close: vi.fn(), + probeAvailability: vi.fn(async () => {}), + isHealthy: vi.fn(() => true), + doctor: vi.fn(async () => ({ ok: true, message: "ok" })), + }; + const runtimeFactory = vi.fn(() => runtime as never); + const service = createAcpxRuntimeService({ + pluginConfig: { probeAgent: "opencode" }, + runtimeFactory, + }); + + await service.start(ctx); + + expect(runtimeFactory).toHaveBeenCalledWith( + expect.objectContaining({ + pluginConfig: expect.objectContaining({ + probeAgent: "opencode", + }), + }), + ); + + await service.stop?.(ctx); + }); + it("warns when legacy compatibility config is explicitly ignored", async () => { const workspaceDir = await makeTempDir(); const ctx = createServiceContext(workspaceDir); diff --git a/extensions/acpx/src/service.ts b/extensions/acpx/src/service.ts index 27175434b9a..576e4072f19 100644 --- a/extensions/acpx/src/service.ts +++ b/extensions/acpx/src/service.ts @@ -53,6 +53,7 @@ function createDefaultRuntime(params: AcpxRuntimeFactoryParams): AcpxRuntimeLike mcpServers: toAcpMcpServers(params.pluginConfig.mcpServers), permissionMode: params.pluginConfig.permissionMode, nonInteractivePermissions: params.pluginConfig.nonInteractivePermissions, + probeAgent: params.pluginConfig.probeAgent, timeoutMs: params.pluginConfig.timeoutSeconds != null ? params.pluginConfig.timeoutSeconds * 1_000