@@ -5,6 +5,11 @@ import type { ResolvedGoogleChatAccount } from "./accounts.js";
55import { deleteGoogleChatMessage , sendGoogleChatMessage , updateGoogleChatMessage } from "./api.js" ;
66import type { GoogleChatCoreRuntime , GoogleChatRuntimeEnv } from "./monitor-types.js" ;
77
8+ export type GoogleChatTypingMessage = {
9+ name : string ;
10+ thread ?: string ;
11+ } ;
12+
813export async function deliverGoogleChatReply ( params : {
914 payload : {
1015 text ?: string ;
@@ -18,16 +23,29 @@ export async function deliverGoogleChatReply(params: {
1823 core : GoogleChatCoreRuntime ;
1924 config : OpenClawConfig ;
2025 statusSink ?: ( patch : { lastInboundAt ?: number ; lastOutboundAt ?: number } ) => void ;
21- typingMessageName ?: string ;
26+ typingMessage ?: GoogleChatTypingMessage ;
2227} ) : Promise < void > {
2328 const { payload, account, spaceId, runtime, core, config, statusSink } = params ;
2429 // Clear this whenever the typing message is deleted or unavailable; otherwise
2530 // text delivery can keep retrying a dead message and drop content.
26- let typingMessageName = params . typingMessageName ;
31+ let typingMessage = params . typingMessage ;
32+ const replyThreadName = payload . replyToId ?. trim ( ) || undefined ;
33+ const typingMessageThreadName = typingMessage ?. thread ?. trim ( ) || undefined ;
2734 const reply = resolveSendableOutboundReplyParts ( payload ) ;
2835 const text = reply . text ;
2936 let firstTextChunk = true ;
3037
38+ if ( typingMessage && typingMessageThreadName !== replyThreadName ) {
39+ // Typing starts before reply directives are resolved. Never edit a placeholder
40+ // from one thread into a final reply targeted at another conversation surface.
41+ try {
42+ await deleteGoogleChatMessage ( { account, messageName : typingMessage . name } ) ;
43+ } catch ( err ) {
44+ runtime . error ?.( `Google Chat typing cleanup failed: ${ String ( err ) } ` ) ;
45+ }
46+ typingMessage = undefined ;
47+ }
48+
3149 if ( reply . hasMedia ) {
3250 runtime . error ?.(
3351 "Google Chat outbound attachments require user OAuth and are not supported by this service-account channel; sending text fallback only." ,
@@ -36,8 +54,8 @@ export async function deliverGoogleChatReply(params: {
3654
3755 if ( reply . hasMedia && ! reply . hasText ) {
3856 try {
39- if ( typingMessageName ) {
40- await deleteGoogleChatMessage ( { account, messageName : typingMessageName } ) ;
57+ if ( typingMessage ) {
58+ await deleteGoogleChatMessage ( { account, messageName : typingMessage . name } ) ;
4159 }
4260 } catch ( err ) {
4361 runtime . error ?.( `Google Chat typing cleanup failed: ${ String ( err ) } ` ) ;
@@ -54,7 +72,7 @@ export async function deliverGoogleChatReply(params: {
5472 account,
5573 space : spaceId ,
5674 text : chunk ,
57- thread : payload . replyToId ,
75+ thread : replyThreadName ,
5876 } ) ;
5977 } ;
6078 const chunks = core . channel . text . chunkMarkdownTextWithMode ( text , chunkLimit , chunkMode ) ;
@@ -63,10 +81,10 @@ export async function deliverGoogleChatReply(params: {
6381 continue ;
6482 }
6583 try {
66- if ( firstTextChunk && typingMessageName ) {
84+ if ( firstTextChunk && typingMessage ) {
6785 await updateGoogleChatMessage ( {
6886 account,
69- messageName : typingMessageName ,
87+ messageName : typingMessage . name ,
7088 text : chunk ,
7189 } ) ;
7290 } else {
@@ -76,8 +94,8 @@ export async function deliverGoogleChatReply(params: {
7694 statusSink ?.( { lastOutboundAt : Date . now ( ) } ) ;
7795 } catch ( err ) {
7896 runtime . error ?.( `Google Chat message send failed: ${ String ( err ) } ` ) ;
79- if ( firstTextChunk && typingMessageName ) {
80- typingMessageName = undefined ;
97+ if ( firstTextChunk && typingMessage ) {
98+ typingMessage = undefined ;
8199 try {
82100 await sendTextMessage ( chunk ) ;
83101 statusSink ?.( { lastOutboundAt : Date . now ( ) } ) ;
0 commit comments