@@ -235,6 +235,7 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
235235 info : { kind : "block" | "final" } ,
236236 ) => Promise < ReplyPayload | null > | ReplyPayload | null ;
237237 deliver : ( payload : unknown , info : { kind : "block" | "final" } ) => Promise < void > | void ;
238+ onError ?: ( err : unknown , info : { kind : "block" | "final" } ) => void ;
238239 transformReplyPayload ?: ( payload : ReplyPayload ) => ReplyPayload | null ;
239240 onSettled ?: ( ) => Promise < unknown > | unknown ;
240241 } ;
@@ -258,7 +259,9 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
258259 await params . dispatcherOptions . deliver ( deliverPayload , info ) ;
259260 } ;
260261 const queueDelivery = ( payload : ReplyPayload , info : { kind : "block" | "final" } ) => {
261- const delivery = Promise . resolve ( deliver ( payload , info ) ) . catch ( ( ) => undefined ) ;
262+ const delivery = Promise . resolve ( deliver ( payload , info ) ) . catch ( ( err : unknown ) => {
263+ params . dispatcherOptions . onError ?.( err , info ) ;
264+ } ) ;
262265 pendingDeliveries . push ( delivery ) ;
263266 return true ;
264267 } ;
@@ -2325,6 +2328,39 @@ describe("processDiscordMessage draft streaming", () => {
23252328 } ) ;
23262329 } ) ;
23272330
2331+ it ( "delivers tool warning finals when the recovered reply fails to send" , async ( ) => {
2332+ deliverDiscordReply . mockRejectedValueOnce ( new Error ( "send failed" ) ) ;
2333+ dispatchInboundMessage . mockImplementationOnce ( async ( params ?: DispatchInboundParams ) => {
2334+ await params ?. dispatcher . sendFinalReply ( { text : "delivery failed" } ) ;
2335+ await params ?. dispatcher . waitForIdle ( ) ;
2336+ await params ?. dispatcher . sendFinalReply ( createNonTerminalToolWarningPayload ( ) ) ;
2337+ return {
2338+ queuedFinal : true ,
2339+ counts : { final : 2 , tool : 0 , block : 0 } ,
2340+ failedCounts : { final : 1 } ,
2341+ } ;
2342+ } ) ;
2343+
2344+ const ctx = await createAutomaticSourceDeliveryContext ( {
2345+ discordConfig : { streamMode : "off" } ,
2346+ } ) ;
2347+
2348+ await runProcessDiscordMessage ( ctx ) ;
2349+
2350+ expect ( deliverDiscordReply ) . toHaveBeenCalledTimes ( 2 ) ;
2351+ expect ( firstMockArg ( deliverDiscordReply , "deliverDiscordReply" ) ) . toMatchObject ( {
2352+ replies : [ { text : "delivery failed" } ] ,
2353+ } ) ;
2354+ expect ( deliverDiscordReply . mock . calls [ 1 ] ?. [ 0 ] ) . toMatchObject ( {
2355+ replies : [
2356+ {
2357+ text : "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed" ,
2358+ isError : true ,
2359+ } ,
2360+ ] ,
2361+ } ) ;
2362+ } ) ;
2363+
23282364 it ( "keeps mutating tool warning finals after successful-looking replies" , async ( ) => {
23292365 const draftStream = createMockDraftStreamForTest ( ) ;
23302366 dispatchInboundMessage . mockImplementationOnce ( async ( params ?: DispatchInboundParams ) => {
0 commit comments