diff --git a/extensions/googlechat/src/channel.adapters.ts b/extensions/googlechat/src/channel.adapters.ts index b203ce98a58..a1b91dbf25e 100644 --- a/extensions/googlechat/src/channel.adapters.ts +++ b/extensions/googlechat/src/channel.adapters.ts @@ -1,4 +1,8 @@ import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers"; +import type { + ChannelThreadingContext, + ChannelThreadingToolContext, +} from "openclaw/plugin-sdk/channel-contract"; import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, @@ -127,6 +131,29 @@ export const googlechatThreadingAdapter = { account.config.replyToMode, fallback: "off" as const, }, + buildToolContext: ({ + cfg, + accountId, + context, + hasRepliedRef, + }: { + cfg: OpenClawConfig; + accountId?: string | null; + context: ChannelThreadingContext; + hasRepliedRef?: { value: boolean }; + }): ChannelThreadingToolContext => { + const currentChannelId = normalizeGoogleChatTarget(context.To); + const replyToId = + normalizeOptionalString(context.ReplyToIdFull) ?? normalizeOptionalString(context.ReplyToId); + + return { + currentChannelId, + currentMessageId: replyToId, + currentThreadTs: replyToId, + replyToMode: resolveGoogleChatAccount({ cfg, accountId }).config.replyToMode, + hasRepliedRef, + }; + }, }; export const googlechatPairingTextAdapter = { diff --git a/extensions/googlechat/src/channel.test.ts b/extensions/googlechat/src/channel.test.ts index c9611ec299e..6d5164c77e8 100644 --- a/extensions/googlechat/src/channel.test.ts +++ b/extensions/googlechat/src/channel.test.ts @@ -439,6 +439,62 @@ describe("googlechatPlugin threading", () => { googlechatThreadingAdapter.scopedAccountReplyToMode.resolveReplyToMode(defaultAccount), ).toBe("all"); }); + + it("uses the inbound thread resource as the current tool reply target", () => { + const cfg = { + channels: { + googlechat: { + replyToMode: "all", + }, + }, + } as OpenClawConfig; + const hasRepliedRef = { value: false }; + + const context = googlechatThreadingAdapter.buildToolContext({ + cfg, + accountId: "default", + context: { + To: "googlechat:spaces/AAA", + CurrentMessageId: "spaces/AAA/messages/msg-1", + ReplyToId: "spaces/AAA/threads/thread-1", + }, + hasRepliedRef, + }); + + expect(context).toMatchObject({ + currentChannelId: "spaces/AAA", + currentMessageId: "spaces/AAA/threads/thread-1", + currentThreadTs: "spaces/AAA/threads/thread-1", + replyToMode: "all", + hasRepliedRef, + }); + }); + + it("does not use message resources as implicit Google Chat reply targets", () => { + const cfg = { + channels: { + googlechat: { + replyToMode: "all", + }, + }, + } as OpenClawConfig; + + const context = googlechatThreadingAdapter.buildToolContext({ + cfg, + accountId: "default", + context: { + To: "googlechat:spaces/AAA", + CurrentMessageId: "spaces/AAA/messages/msg-1", + }, + }); + + expect(context).toMatchObject({ + currentChannelId: "spaces/AAA", + replyToMode: "all", + }); + expect(context.currentMessageId).toBeUndefined(); + expect(context.currentThreadTs).toBeUndefined(); + }); }); const resolveTarget = googlechatOutboundAdapter.base.resolveTarget; diff --git a/src/auto-reply/reply/agent-runner-utils.ts b/src/auto-reply/reply/agent-runner-utils.ts index bde9793a347..51660728992 100644 --- a/src/auto-reply/reply/agent-runner-utils.ts +++ b/src/auto-reply/reply/agent-runner-utils.ts @@ -152,6 +152,7 @@ export function buildThreadingToolContext(params: { ChatType: sessionCtx.ChatType, CurrentMessageId: currentMessageId, ReplyToId: sessionCtx.ReplyToId, + ReplyToIdFull: sessionCtx.ReplyToIdFull, ThreadLabel: sessionCtx.ThreadLabel, MessageThreadId: sessionCtx.MessageThreadId, TransportThreadId: sessionCtx.TransportThreadId, @@ -159,10 +160,13 @@ export function buildThreadingToolContext(params: { }, hasRepliedRef, }) ?? {}; + const hasAdapterCurrentMessageId = Object.hasOwn(context, "currentMessageId"); return { ...context, currentChannelProvider: provider!, // guaranteed non-null since threading exists - currentMessageId: context.currentMessageId ?? currentMessageId, + // Some providers expose only thread resources as reply targets; explicit + // `undefined` means the adapter rejected the generic message-id fallback. + currentMessageId: hasAdapterCurrentMessageId ? context.currentMessageId : currentMessageId, }; } diff --git a/src/auto-reply/reply/reply-plumbing.test.ts b/src/auto-reply/reply/reply-plumbing.test.ts index 5eff8ce5b8e..cc71780d311 100644 --- a/src/auto-reply/reply/reply-plumbing.test.ts +++ b/src/auto-reply/reply/reply-plumbing.test.ts @@ -193,6 +193,44 @@ describe("buildThreadingToolContext", () => { expect(result.currentChannelId).toBe("C1"); expect(result.currentThreadTs).toBe("123.456"); }); + + it("lets plugin threading adapters suppress the generic message-id fallback", () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "googlechat", + plugin: { + ...createChannelTestPluginBase({ id: "googlechat", label: "Google Chat" }), + threading: { + buildToolContext: ({ context }) => ({ + currentChannelId: context.To?.replace(/^googlechat:/, ""), + currentMessageId: undefined, + currentThreadTs: context.ReplyToIdFull ?? context.ReplyToId, + }), + }, + } as ChannelPlugin, + source: "test", + }, + ]), + ); + const sessionCtx = { + Provider: "googlechat", + To: "googlechat:spaces/AAA", + MessageSidFull: "spaces/AAA/messages/msg-1", + ReplyToId: "spaces/AAA/threads/short", + ReplyToIdFull: "spaces/AAA/threads/full", + } as TemplateContext; + + const result = buildThreadingToolContext({ + sessionCtx, + config: { channels: { googlechat: { replyToMode: "all" } } } as OpenClawConfig, + hasRepliedRef: undefined, + }); + + expect(result.currentChannelId).toBe("spaces/AAA"); + expect(result.currentThreadTs).toBe("spaces/AAA/threads/full"); + expect(result.currentMessageId).toBeUndefined(); + }); }); describe("applyReplyThreading auto-threading", () => {