From 574ea852b31b8d1255bbbf8243ded2e75e457385 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:49:21 -0500 Subject: [PATCH] fix(feishu): preserve sender-scoped ACP rebinding --- extensions/feishu/src/bot.ts | 3 +++ src/acp/persistent-bindings.resolve.ts | 3 +++ .../reply/commands-acp/context.test.ts | 23 +++++++++++++++++++ src/auto-reply/reply/commands-acp/context.ts | 10 ++++---- src/config/zod-schema.agents.ts | 2 +- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index a55872599c8..fc84801b124 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -1262,6 +1262,9 @@ export async function handleFeishuMessage(params: { configuredBinding = configuredRoute.configuredBinding; route = configuredRoute.route; + // Bound Feishu conversations intentionally require an exact live conversation-id match. + // Sender-scoped topic sessions therefore bind on `chat:topic:root:sender:user`, while + // configured ACP bindings may still inherit the shared `chat:topic:root` topic session. const threadBinding = getSessionBindingService().resolveByConversation({ channel: "feishu", accountId: account.accountId, diff --git a/src/acp/persistent-bindings.resolve.ts b/src/acp/persistent-bindings.resolve.ts index 4af2883d90d..2429945e6db 100644 --- a/src/acp/persistent-bindings.resolve.ts +++ b/src/acp/persistent-bindings.resolve.ts @@ -247,6 +247,9 @@ export function resolveConfiguredAcpBindingSpecBySessionKey(params: { channel: "feishu", accountId: parsedSessionKey.accountId, conversationId: targetParsed.canonicalConversationId, + // Session-key recovery deliberately collapses sender-scoped topic bindings onto the + // canonical topic conversation id so `group_topic` and `group_topic_sender` reuse + // the same configured ACP session identity. parentConversationId: targetParsed.scope === "group_topic" || targetParsed.scope === "group_topic_sender" ? targetParsed.chatId diff --git a/src/auto-reply/reply/commands-acp/context.test.ts b/src/auto-reply/reply/commands-acp/context.test.ts index 9c3dcdb84bb..1ac682d117f 100644 --- a/src/auto-reply/reply/commands-acp/context.test.ts +++ b/src/auto-reply/reply/commands-acp/context.test.ts @@ -174,6 +174,29 @@ describe("commands-acp context", () => { ); }); + it("preserves sender-scoped Feishu topic ids after ACP route takeover via ParentSessionKey", () => { + const params = buildCommandTestParams("/acp status", baseCfg, { + Provider: "feishu", + Surface: "feishu", + OriginatingChannel: "feishu", + OriginatingTo: "chat:oc_group_chat", + MessageThreadId: "om_topic_root", + SenderId: "ou_topic_user", + AccountId: "work", + ParentSessionKey: + "agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user", + }); + params.sessionKey = "agent:codex:acp:binding:feishu:work:abc123"; + + expect(resolveAcpCommandBindingContext(params)).toEqual({ + channel: "feishu", + accountId: "work", + threadId: "om_topic_root", + conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user", + parentConversationId: "oc_group_chat", + }); + }); + it("resolves Feishu DM conversation ids from user targets", () => { const params = buildCommandTestParams("/acp status", baseCfg, { Provider: "feishu", diff --git a/src/auto-reply/reply/commands-acp/context.ts b/src/auto-reply/reply/commands-acp/context.ts index 33c4bdf85ba..d27ed5b2555 100644 --- a/src/auto-reply/reply/commands-acp/context.ts +++ b/src/auto-reply/reply/commands-acp/context.ts @@ -45,15 +45,16 @@ function resolveFeishuSenderScopedConversationId(params: { threadId?: string; senderId?: string; sessionKey?: string; + parentSessionKey?: string; }): string | undefined { const parentConversationId = normalizeConversationText(params.parentConversationId); const threadId = normalizeConversationText(params.threadId); const senderId = normalizeConversationText(params.senderId); - const scopedRest = parseAgentSessionKey(params.sessionKey)?.rest?.trim().toLowerCase() ?? ""; const expectedScopePrefix = `feishu:group:${parentConversationId?.toLowerCase()}:topic:${threadId?.toLowerCase()}:sender:`; - const isSenderScopedSession = Boolean( - scopedRest && expectedScopePrefix && scopedRest.startsWith(expectedScopePrefix), - ); + const isSenderScopedSession = [params.sessionKey, params.parentSessionKey].some((candidate) => { + const scopedRest = parseAgentSessionKey(candidate)?.rest?.trim().toLowerCase() ?? ""; + return Boolean(scopedRest && expectedScopePrefix && scopedRest.startsWith(expectedScopePrefix)); + }); if (!parentConversationId || !threadId || !senderId || !isSenderScopedSession) { return undefined; } @@ -123,6 +124,7 @@ export function resolveAcpCommandConversationId(params: HandleCommandsParams): s threadId, senderId: params.command.senderId ?? params.ctx.SenderId, sessionKey: params.sessionKey, + parentSessionKey: params.ctx.ParentSessionKey, }); return ( senderScopedConversationId ?? diff --git a/src/config/zod-schema.agents.ts b/src/config/zod-schema.agents.ts index ebdaadfe27d..66bd586fa04 100644 --- a/src/config/zod-schema.agents.ts +++ b/src/config/zod-schema.agents.ts @@ -90,7 +90,7 @@ const AcpBindingSchema = z } if ( channel === "feishu" && - !/^(ou_[^:]+|on_[^:]+|[^:]+:topic:[^:]+(?::sender:[^:]+)?)$/.test(peerId) + !/^(ou_[^:]+|on_[^:]+|oc_[^:]+:topic:[^:]+(?::sender:[^:]+)?)$/.test(peerId) ) { ctx.addIssue({ code: z.ZodIssueCode.custom,