Skip to content

Commit a71b121

Browse files
fix(googlechat): preserve thread for message tool replies (#80996)
Use the Google Chat thread resource as the ambient message-tool reply target so replies stay in the inbound thread. Normalize the current Google Chat space target and let plugin threading adapters explicitly suppress the generic message-id fallback when a provider needs a thread resource instead of a message resource. Co-authored-by: Peter Steinberger <[email protected]> Co-authored-by: Franco Viotti <[email protected]>
1 parent ed74fa6 commit a71b121

4 files changed

Lines changed: 126 additions & 1 deletion

File tree

extensions/googlechat/src/channel.adapters.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers";
2+
import type {
3+
ChannelThreadingContext,
4+
ChannelThreadingToolContext,
5+
} from "openclaw/plugin-sdk/channel-contract";
26
import {
37
createMessageReceiptFromOutboundResults,
48
defineChannelMessageAdapter,
@@ -127,6 +131,29 @@ export const googlechatThreadingAdapter = {
127131
account.config.replyToMode,
128132
fallback: "off" as const,
129133
},
134+
buildToolContext: ({
135+
cfg,
136+
accountId,
137+
context,
138+
hasRepliedRef,
139+
}: {
140+
cfg: OpenClawConfig;
141+
accountId?: string | null;
142+
context: ChannelThreadingContext;
143+
hasRepliedRef?: { value: boolean };
144+
}): ChannelThreadingToolContext => {
145+
const currentChannelId = normalizeGoogleChatTarget(context.To);
146+
const replyToId =
147+
normalizeOptionalString(context.ReplyToIdFull) ?? normalizeOptionalString(context.ReplyToId);
148+
149+
return {
150+
currentChannelId,
151+
currentMessageId: replyToId,
152+
currentThreadTs: replyToId,
153+
replyToMode: resolveGoogleChatAccount({ cfg, accountId }).config.replyToMode,
154+
hasRepliedRef,
155+
};
156+
},
130157
};
131158

132159
export const googlechatPairingTextAdapter = {

extensions/googlechat/src/channel.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,62 @@ describe("googlechatPlugin threading", () => {
439439
googlechatThreadingAdapter.scopedAccountReplyToMode.resolveReplyToMode(defaultAccount),
440440
).toBe("all");
441441
});
442+
443+
it("uses the inbound thread resource as the current tool reply target", () => {
444+
const cfg = {
445+
channels: {
446+
googlechat: {
447+
replyToMode: "all",
448+
},
449+
},
450+
} as OpenClawConfig;
451+
const hasRepliedRef = { value: false };
452+
453+
const context = googlechatThreadingAdapter.buildToolContext({
454+
cfg,
455+
accountId: "default",
456+
context: {
457+
To: "googlechat:spaces/AAA",
458+
CurrentMessageId: "spaces/AAA/messages/msg-1",
459+
ReplyToId: "spaces/AAA/threads/thread-1",
460+
},
461+
hasRepliedRef,
462+
});
463+
464+
expect(context).toMatchObject({
465+
currentChannelId: "spaces/AAA",
466+
currentMessageId: "spaces/AAA/threads/thread-1",
467+
currentThreadTs: "spaces/AAA/threads/thread-1",
468+
replyToMode: "all",
469+
hasRepliedRef,
470+
});
471+
});
472+
473+
it("does not use message resources as implicit Google Chat reply targets", () => {
474+
const cfg = {
475+
channels: {
476+
googlechat: {
477+
replyToMode: "all",
478+
},
479+
},
480+
} as OpenClawConfig;
481+
482+
const context = googlechatThreadingAdapter.buildToolContext({
483+
cfg,
484+
accountId: "default",
485+
context: {
486+
To: "googlechat:spaces/AAA",
487+
CurrentMessageId: "spaces/AAA/messages/msg-1",
488+
},
489+
});
490+
491+
expect(context).toMatchObject({
492+
currentChannelId: "spaces/AAA",
493+
replyToMode: "all",
494+
});
495+
expect(context.currentMessageId).toBeUndefined();
496+
expect(context.currentThreadTs).toBeUndefined();
497+
});
442498
});
443499

444500
const resolveTarget = googlechatOutboundAdapter.base.resolveTarget;

src/auto-reply/reply/agent-runner-utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,21 @@ export function buildThreadingToolContext(params: {
152152
ChatType: sessionCtx.ChatType,
153153
CurrentMessageId: currentMessageId,
154154
ReplyToId: sessionCtx.ReplyToId,
155+
ReplyToIdFull: sessionCtx.ReplyToIdFull,
155156
ThreadLabel: sessionCtx.ThreadLabel,
156157
MessageThreadId: sessionCtx.MessageThreadId,
157158
TransportThreadId: sessionCtx.TransportThreadId,
158159
NativeChannelId: sessionCtx.NativeChannelId,
159160
},
160161
hasRepliedRef,
161162
}) ?? {};
163+
const hasAdapterCurrentMessageId = Object.hasOwn(context, "currentMessageId");
162164
return {
163165
...context,
164166
currentChannelProvider: provider!, // guaranteed non-null since threading exists
165-
currentMessageId: context.currentMessageId ?? currentMessageId,
167+
// Some providers expose only thread resources as reply targets; explicit
168+
// `undefined` means the adapter rejected the generic message-id fallback.
169+
currentMessageId: hasAdapterCurrentMessageId ? context.currentMessageId : currentMessageId,
166170
};
167171
}
168172

src/auto-reply/reply/reply-plumbing.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,44 @@ describe("buildThreadingToolContext", () => {
193193
expect(result.currentChannelId).toBe("C1");
194194
expect(result.currentThreadTs).toBe("123.456");
195195
});
196+
197+
it("lets plugin threading adapters suppress the generic message-id fallback", () => {
198+
setActivePluginRegistry(
199+
createTestRegistry([
200+
{
201+
pluginId: "googlechat",
202+
plugin: {
203+
...createChannelTestPluginBase({ id: "googlechat", label: "Google Chat" }),
204+
threading: {
205+
buildToolContext: ({ context }) => ({
206+
currentChannelId: context.To?.replace(/^googlechat:/, ""),
207+
currentMessageId: undefined,
208+
currentThreadTs: context.ReplyToIdFull ?? context.ReplyToId,
209+
}),
210+
},
211+
} as ChannelPlugin,
212+
source: "test",
213+
},
214+
]),
215+
);
216+
const sessionCtx = {
217+
Provider: "googlechat",
218+
To: "googlechat:spaces/AAA",
219+
MessageSidFull: "spaces/AAA/messages/msg-1",
220+
ReplyToId: "spaces/AAA/threads/short",
221+
ReplyToIdFull: "spaces/AAA/threads/full",
222+
} as TemplateContext;
223+
224+
const result = buildThreadingToolContext({
225+
sessionCtx,
226+
config: { channels: { googlechat: { replyToMode: "all" } } } as OpenClawConfig,
227+
hasRepliedRef: undefined,
228+
});
229+
230+
expect(result.currentChannelId).toBe("spaces/AAA");
231+
expect(result.currentThreadTs).toBe("spaces/AAA/threads/full");
232+
expect(result.currentMessageId).toBeUndefined();
233+
});
196234
});
197235

198236
describe("applyReplyThreading auto-threading", () => {

0 commit comments

Comments
 (0)