Skip to content

Commit ca92a51

Browse files
fix(clawsweeper): address review for gitcrawl-4952-google-chat-dm-threading-broken-each-response-creates-new-thread (1)
Co-authored-by: Jai Govindani <[email protected]> Co-authored-by: banddude <[email protected]> Co-authored-by: Neerav Makwana <[email protected]> Co-authored-by: Novan Adrian <[email protected]> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
1 parent 48d9625 commit ca92a51

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

extensions/googlechat/src/monitor-reply-delivery.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,35 @@ export async function deliverGoogleChatReply(params: {
2727
config: OpenClawConfig;
2828
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
2929
typingMessageName?: string;
30+
typingMessageThreadName?: string;
3031
}): Promise<void> {
3132
const { payload, account, spaceId, runtime, core, config, statusSink } = params;
3233
// Clear this whenever the typing message is deleted or unavailable; otherwise
3334
// text delivery can keep retrying a dead message and drop content.
3435
let typingMessageName = params.typingMessageName;
36+
const replyThreadName = payload.replyToId?.trim() || undefined;
37+
const typingMessageThreadName = params.typingMessageThreadName?.trim() || undefined;
3538
const reply = resolveSendableOutboundReplyParts(payload);
3639
const mediaCount = reply.mediaCount;
3740
const hasMedia = reply.hasMedia;
3841
const text = reply.text;
3942
let firstTextChunk = true;
4043
let suppressCaption = false;
4144

45+
if (typingMessageName && typingMessageThreadName !== replyThreadName) {
46+
// The typing placeholder was created before replyToMode filtered the payload.
47+
// Do not edit a stale threaded placeholder into the final assistant reply.
48+
try {
49+
await deleteGoogleChatMessage({
50+
account,
51+
messageName: typingMessageName,
52+
});
53+
} catch (err) {
54+
runtime.error?.(`Google Chat typing cleanup failed: ${String(err)}`);
55+
}
56+
typingMessageName = undefined;
57+
}
58+
4259
if (hasMedia && typingMessageName) {
4360
try {
4461
await deleteGoogleChatMessage({
@@ -76,7 +93,7 @@ export async function deliverGoogleChatReply(params: {
7693
account,
7794
space: spaceId,
7895
text: chunk,
79-
thread: payload.replyToId,
96+
thread: replyThreadName,
8097
});
8198
};
8299
await deliverTextOrMediaReply({
@@ -131,7 +148,7 @@ export async function deliverGoogleChatReply(params: {
131148
account,
132149
space: spaceId,
133150
text: caption,
134-
thread: payload.replyToId,
151+
thread: replyThreadName,
135152
attachments: [
136153
{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.fileName },
137154
],

extensions/googlechat/src/monitor.reply-delivery.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ describe("Google Chat reply delivery", () => {
8080
config,
8181
statusSink,
8282
typingMessageName: "spaces/AAA/messages/typing",
83+
typingMessageThreadName: "spaces/AAA/threads/root",
8384
});
8485

8586
expect(mocks.updateGoogleChatMessage).toHaveBeenCalledWith({
@@ -106,6 +107,36 @@ describe("Google Chat reply delivery", () => {
106107
);
107108
});
108109

110+
it("does not update a threaded typing message when replyToMode removed the reply target", async () => {
111+
const core = createCore();
112+
const runtime = createRuntime();
113+
mocks.deleteGoogleChatMessage.mockResolvedValue(undefined);
114+
mocks.sendGoogleChatMessage.mockResolvedValue({ messageName: "spaces/AAA/messages/reply" });
115+
116+
await deliverGoogleChatReply({
117+
payload: { text: "off-mode reply" },
118+
account,
119+
spaceId: "spaces/AAA",
120+
runtime,
121+
core,
122+
config,
123+
typingMessageName: "spaces/AAA/messages/typing",
124+
typingMessageThreadName: "spaces/AAA/threads/root",
125+
});
126+
127+
expect(mocks.deleteGoogleChatMessage).toHaveBeenCalledWith({
128+
account,
129+
messageName: "spaces/AAA/messages/typing",
130+
});
131+
expect(mocks.updateGoogleChatMessage).not.toHaveBeenCalled();
132+
expect(mocks.sendGoogleChatMessage).toHaveBeenCalledWith({
133+
account,
134+
space: "spaces/AAA",
135+
text: "off-mode reply",
136+
thread: undefined,
137+
});
138+
});
139+
109140
it("does not update a deleted typing message before sending media with a caption", async () => {
110141
const core = createCore({
111142
media: { buffer: Buffer.from("image"), contentType: "image/png", fileName: "reply.png" },

extensions/googlechat/src/monitor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ async function processMessageWithPipeline(params: {
430430
config,
431431
statusSink,
432432
typingMessageName,
433+
typingMessageThreadName: replyThreadName,
433434
});
434435
// Only use typing message for first delivery
435436
typingMessageName = undefined;

0 commit comments

Comments
 (0)