From 6afff0642ea34aa4ac07ee46aa0df9d9d0a0a97a Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:11:51 -0500 Subject: [PATCH] fix: preserve account binding metadata on rebind --- .../src/conversation-bindings.test.ts | 60 +++++++++++++++++++ .../account-scoped-conversation-bindings.ts | 9 ++- 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 extensions/bluebubbles/src/conversation-bindings.test.ts diff --git a/extensions/bluebubbles/src/conversation-bindings.test.ts b/extensions/bluebubbles/src/conversation-bindings.test.ts new file mode 100644 index 00000000000..c8480ca6eb1 --- /dev/null +++ b/extensions/bluebubbles/src/conversation-bindings.test.ts @@ -0,0 +1,60 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime"; +import { beforeEach, describe, expect, it } from "vitest"; +import { __testing, createBlueBubblesConversationBindingManager } from "./conversation-bindings.js"; + +const baseCfg = { + session: { mainKey: "main", scope: "per-sender" }, +} satisfies OpenClawConfig; + +describe("BlueBubbles conversation bindings", () => { + beforeEach(() => { + __testing.resetBlueBubblesConversationBindingsForTests(); + }); + + it("preserves existing metadata when rebinding the same conversation", async () => { + const manager = createBlueBubblesConversationBindingManager({ + cfg: baseCfg, + accountId: "default", + }); + + manager.bindConversation({ + conversationId: "chat-guid-1", + targetKind: "subagent", + targetSessionKey: "agent:main:subagent:child", + metadata: { + agentId: "codex", + label: "child", + boundBy: "system", + }, + }); + + await getSessionBindingService().bind({ + targetSessionKey: "agent:main:subagent:child", + targetKind: "subagent", + conversation: { + channel: "bluebubbles", + accountId: "default", + conversationId: "chat-guid-1", + }, + placement: "current", + metadata: { + label: "child", + }, + }); + + expect( + getSessionBindingService().resolveByConversation({ + channel: "bluebubbles", + accountId: "default", + conversationId: "chat-guid-1", + }), + ).toMatchObject({ + metadata: expect.objectContaining({ + agentId: "codex", + label: "child", + boundBy: "system", + }), + }); + }); +}); diff --git a/src/infra/outbound/account-scoped-conversation-bindings.ts b/src/infra/outbound/account-scoped-conversation-bindings.ts index 64d9e726ff2..c4843dd23d0 100644 --- a/src/infra/outbound/account-scoped-conversation-bindings.ts +++ b/src/infra/outbound/account-scoped-conversation-bindings.ts @@ -158,6 +158,9 @@ export function createAccountScopedConversationBindingManager(params.stateKey).bindingsByAccountConversation.get( + resolveBindingKey({ accountId, conversationId: normalizedConversationId }), + ); const now = Date.now(); const record: AccountScopedConversationBindingRecord = { accountId, @@ -167,15 +170,15 @@ export function createAccountScopedConversationBindingManager