diff --git a/src/agents/cli-backends.test.ts b/src/agents/cli-backends.test.ts index 0a554b6f2f0..1e47ae4b872 100644 --- a/src/agents/cli-backends.test.ts +++ b/src/agents/cli-backends.test.ts @@ -288,6 +288,29 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved?.config.args).not.toContain("bypassPermissions"); expect(resolved?.config.resumeArgs).not.toContain("bypassPermissions"); }); + + it("keeps bundle MCP enabled for override-only claude-cli config when the plugin registry is absent", () => { + const registry = createEmptyPluginRegistry(); + setActivePluginRegistry(registry); + + const cfg = { + agents: { + defaults: { + cliBackends: { + "claude-cli": { + command: "/usr/local/bin/claude", + args: ["-p", "--output-format", "json"], + }, + }, + }, + }, + } satisfies OpenClawConfig; + + const resolved = resolveCliBackendConfig("claude-cli", cfg); + + expect(resolved).not.toBeNull(); + expect(resolved?.bundleMcp).toBe(true); + }); }); describe("resolveCliBackendConfig google-gemini-cli defaults", () => { diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index 22aa5168854..1bd9286f6e2 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -10,6 +10,13 @@ export type ResolvedCliBackend = { pluginId?: string; }; +function resolveFallbackBundleMcpCapability(provider: string): boolean { + // Claude CLI consumes explicit MCP config overlays even when the runtime + // plugin registry is not initialized yet (for example direct runner tests or + // narrow non-gateway entrypoints). + return provider === "claude-cli"; +} + function normalizeBackendKey(key: string): string { return normalizeProviderId(key); } @@ -114,5 +121,9 @@ export function resolveCliBackendConfig( if (!command) { return null; } - return { id: normalized, config: { ...override, command }, bundleMcp: false }; + return { + id: normalized, + config: { ...override, command }, + bundleMcp: resolveFallbackBundleMcpCapability(normalized), + }; } diff --git a/src/gateway/gateway-acp-bind.live.test.ts b/src/gateway/gateway-acp-bind.live.test.ts index 7d368a7b570..e9ff122933d 100644 --- a/src/gateway/gateway-acp-bind.live.test.ts +++ b/src/gateway/gateway-acp-bind.live.test.ts @@ -142,7 +142,16 @@ async function waitForAcpBackendHealthy(timeoutMs = 60_000): Promise { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { const backend = getAcpRuntimeBackend("acpx"); - if (backend && (!backend.healthy || backend.healthy())) { + if (backend?.healthy?.() ?? false) { + return; + } + const runtime = backend?.runtime as { probeAvailability?: () => Promise } | undefined; + if (runtime?.probeAvailability) { + await runtime.probeAvailability().catch(() => {}); + if (backend?.healthy?.() ?? false) { + return; + } + } else if (backend && !backend.healthy) { return; } await sleep(250); diff --git a/src/infra/outbound/current-conversation-bindings.test.ts b/src/infra/outbound/current-conversation-bindings.test.ts index 297b1784ae3..08e79fc66c2 100644 --- a/src/infra/outbound/current-conversation-bindings.test.ts +++ b/src/infra/outbound/current-conversation-bindings.test.ts @@ -76,6 +76,22 @@ describe("generic current-conversation bindings", () => { ).toBeNull(); }); + it("keeps Slack current-conversation binding support when the runtime registry is empty", () => { + setActivePluginRegistry(createTestRegistry([])); + + expect( + getGenericCurrentConversationBindingCapabilities({ + channel: "slack", + accountId: "default", + }), + ).toEqual({ + adapterAvailable: true, + bindSupported: true, + unbindSupported: true, + placements: ["current"], + }); + }); + it("reloads persisted bindings after the in-memory cache is cleared", async () => { const bound = await bindGenericCurrentConversation({ targetSessionKey: "agent:codex:acp:slack-dm", diff --git a/src/infra/outbound/current-conversation-bindings.ts b/src/infra/outbound/current-conversation-bindings.ts index 0f6817d6e9f..ee8f72094aa 100644 --- a/src/infra/outbound/current-conversation-bindings.ts +++ b/src/infra/outbound/current-conversation-bindings.ts @@ -22,6 +22,7 @@ type PersistedCurrentConversationBindingsFile = { const CURRENT_BINDINGS_FILE_VERSION = 1; const CURRENT_BINDINGS_ID_PREFIX = "generic:"; +const FALLBACK_CURRENT_CONVERSATION_BINDING_CHANNELS = new Set(["slack"]); let bindingsLoaded = false; let persistPromise: Promise = Promise.resolve(); @@ -132,7 +133,13 @@ function resolveChannelSupportsCurrentConversationBinding(channel: string): bool const plugin = getActivePluginChannelRegistry()?.channels.find((entry) => matchesPluginId(entry.plugin), )?.plugin; - return plugin?.conversationBindings?.supportsCurrentConversationBinding === true; + if (plugin?.conversationBindings?.supportsCurrentConversationBinding === true) { + return true; + } + // Slack live/gateway tests intentionally skip channel startup, so there is no + // active runtime plugin snapshot even though the generic current-conversation + // path is still expected to work. + return FALLBACK_CURRENT_CONVERSATION_BINDING_CHANNELS.has(normalized); } export function getGenericCurrentConversationBindingCapabilities(params: {