From d50526dddc720a6daeee770558b63574e0ef6d05 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:08:17 -0500 Subject: [PATCH] fix(regression): use active channel registry for generic bindings --- .../outbound/session-binding-service.test.ts | 50 +++++++++++++++++++ src/infra/outbound/session-binding-service.ts | 10 ++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/infra/outbound/session-binding-service.test.ts b/src/infra/outbound/session-binding-service.test.ts index b0988d198c0..0f4036addc4 100644 --- a/src/infra/outbound/session-binding-service.test.ts +++ b/src/infra/outbound/session-binding-service.test.ts @@ -1,5 +1,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { setDefaultChannelPluginRegistryForTests } from "../../commands/channel-test-helpers.js"; +import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js"; +import { + pinActivePluginChannelRegistry, + releasePinnedPluginChannelRegistry, + setActivePluginRegistry, +} from "../../plugins/runtime.js"; import { __testing, getSessionBindingService, @@ -358,6 +364,50 @@ describe("session binding service", () => { }); }); + it("does not advertise generic plugin bindings from a stale global registry when the active channel registry is empty", async () => { + const activeRegistry = createEmptyPluginRegistry(); + activeRegistry.channels.push({ + plugin: { + id: "external-chat", + meta: { aliases: ["external-chat-alias"] }, + } as never, + } as never); + setActivePluginRegistry(activeRegistry); + const pinnedEmptyChannelRegistry = createEmptyPluginRegistry(); + pinActivePluginChannelRegistry(pinnedEmptyChannelRegistry); + + try { + const service = getSessionBindingService(); + expect( + service.getCapabilities({ + channel: "external-chat-alias", + accountId: "default", + }), + ).toEqual({ + adapterAvailable: false, + bindSupported: false, + unbindSupported: false, + placements: [], + }); + + await expect( + service.bind({ + targetSessionKey: "agent:codex:acp:external-chat", + targetKind: "session", + conversation: { + channel: "external-chat-alias", + accountId: "default", + conversationId: "room-1", + }, + }), + ).rejects.toMatchObject({ + code: "BINDING_ADAPTER_UNAVAILABLE", + }); + } finally { + releasePinnedPluginChannelRegistry(pinnedEmptyChannelRegistry); + } + }); + it("keeps the first live adapter authoritative until it unregisters", () => { const firstBinding = { bindingId: "first-binding", diff --git a/src/infra/outbound/session-binding-service.ts b/src/infra/outbound/session-binding-service.ts index c1dd123bd17..c51d96fce28 100644 --- a/src/infra/outbound/session-binding-service.ts +++ b/src/infra/outbound/session-binding-service.ts @@ -1,4 +1,4 @@ -import { normalizeAnyChannelId, normalizeChannelId } from "../../channels/registry.js"; +import { normalizeChannelId } from "../../channels/registry.js"; import { getActivePluginChannelRegistry } from "../../plugins/runtime.js"; import { normalizeAccountId } from "../../routing/session-key.js"; import { resolveGlobalMap } from "../../shared/global-singleton.js"; @@ -244,11 +244,15 @@ function supportsGenericCurrentConversationBindings(params: { accountId: string; }): boolean { void params.accountId; + const normalizedChannel = params.channel.trim().toLowerCase(); return Boolean( normalizeChannelId(params.channel) || - normalizeAnyChannelId(params.channel) || getActivePluginChannelRegistry()?.channels.some( - (entry) => entry.plugin.id === params.channel.trim().toLowerCase(), + (entry) => + entry.plugin.id.trim().toLowerCase() === normalizedChannel || + (entry.plugin.meta?.aliases ?? []).some( + (alias) => alias.trim().toLowerCase() === normalizedChannel, + ), ), ); }